diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..9a4e5a2cedc --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,12 @@ +Code of Conduct +=============== + +This project follows a [Code of Conduct][code_of_conduct] in order to ensure an +open and welcoming environment. Please read the full text for understanding the +accepted and unaccepted behavior. + +Please read also the [reporting guidelines][guidelines], in case you encountered +or witnessed any misbehavior. + +[code_of_conduct]: https://symfony.com/doc/current/contributing/code_of_conduct/code_of_conduct.html +[guidelines]: https://symfony.com/doc/current/contributing/code_of_conduct/reporting_guidelines.html diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 54% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index da449d7c265..acb0770920e 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,6 +2,4 @@ Contributing ------------ We love contributors! For more information on how you can contribute to the -Symfony documentation, please read [Contributing to the Documentation](https://symfony.com/doc/current/contributing/documentation/overview.html) -and notice the [Pull Request Format](https://symfony.com/doc/current/contributing/documentation/overview.html#pull-request-format) -that helps us merge your pull requests faster! +Symfony documentation, please read [Contributing to the Documentation](https://symfony.com/doc/current/contributing/documentation/overview.html). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..bc7d6a94182 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + diff --git a/.gitignore b/.gitignore index 805ea28a8f0..a5eb433eea3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -/_build -/_exts +/_build/doctrees +/_build/html +*.pyc diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 486727b665d..00000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "sphinx-php"] - path = _exts - url = http://github.com/fabpot/sphinx-php diff --git a/.platform.app.yaml b/.platform.app.yaml index 946c6f7c152..66f6a036b73 100644 --- a/.platform.app.yaml +++ b/.platform.app.yaml @@ -46,10 +46,16 @@ disk: 512 # Build time dependencies. dependencies: python: - sphinx: ">=1" + virtualenv: 15.1.0 # The hooks that will be performed when the package is deployed. hooks: build: | - pip install git+https://github.com/fabpot/sphinx-php.git - make html + virtualenv .virtualenv + . .virtualenv/bin/activate + # Platform.sh currently sets PIP_USER=1. + export PIP_USER= + pip install pip==9.0.1 wheel==0.29.0 + pip install -r _build/.requirements.txt + find .virtualenv -type f -name "*.rst" -delete + make -C _build html diff --git a/.travis.yml b/.travis.yml index 667ca86fa50..cccb01eef30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,14 @@ language: python -python: "2.7" +python: 2.7 sudo: false - cache: - directories: - - $HOME/.cache/pip - - _build + directories: [$HOME/.cache/pip] -install: pip install sphinx==1.1.3 +install: pip install -r _build/.requirements.txt -script: sphinx-build -nW -b html -d _build/doctrees . _build/html +script: make -C _build SPHINXOPTS=-nW html branches: except: diff --git a/README.markdown b/README.markdown index 92816a63702..8d01c3cdcb7 100644 --- a/README.markdown +++ b/README.markdown @@ -7,8 +7,8 @@ Contributing ------------ >**Note** ->Unless you're documenting a feature that was introduced *after* Symfony 2.3 ->(e.g. in Symfony 2.4), all pull requests must be based off of the **2.3** branch, +>Unless you're documenting a feature that was introduced *after* Symfony 2.7 +>(e.g. in Symfony 2.8), all pull requests must be based off of the **2.7** branch, >**not** the master or older branches. We love contributors! For more information on how you can contribute to the diff --git a/_build/.requirements.txt b/_build/.requirements.txt new file mode 100644 index 00000000000..4a52e3fcb7e --- /dev/null +++ b/_build/.requirements.txt @@ -0,0 +1,13 @@ +alabaster==0.7.10 +Babel==2.4.0 +docutils==0.13.1 +imagesize==0.7.1 +Jinja2==2.9.6 +MarkupSafe==1.0 +Pygments==2.2.0 +pytz==2017.2 +requests==2.12.5 +six==1.10.0 +snowballstemmer==1.2.1 +Sphinx==1.3.6 +git+https://github.com/fabpot/sphinx-php.git@7312eccce9465640752e51373a480da700e02345#egg_name=sphinx-php diff --git a/Makefile b/_build/Makefile similarity index 97% rename from Makefile rename to _build/Makefile index a37807af545..25b660056fe 100644 --- a/Makefile +++ b/_build/Makefile @@ -5,12 +5,12 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = -BUILDDIR = _build +BUILDDIR = . # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -c $(BUILDDIR) -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) ../ # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . diff --git a/_build/_theme/_exts/symfonycom/__init__.py b/_build/_theme/_exts/symfonycom/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/_build/_theme/_exts/symfonycom/sphinx/__init__.py b/_build/_theme/_exts/symfonycom/sphinx/__init__.py new file mode 100644 index 00000000000..1c08bcc11c8 --- /dev/null +++ b/_build/_theme/_exts/symfonycom/sphinx/__init__.py @@ -0,0 +1,167 @@ +from sphinx.highlighting import lexers, PygmentsBridge +from pygments.style import Style +from pygments.formatters import HtmlFormatter +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + +from sphinx.writers.html import HTMLTranslator +from docutils import nodes +from sphinx.locale import admonitionlabels, lazy_gettext + +customadmonitionlabels = admonitionlabels +l_ = lazy_gettext +customadmonitionlabels['best-practice'] = l_('Best Practice') + +def _getType(path): + return path[:path.find('/')] + +def _isIndex(path): + return 'index' in path + +class SensioHTMLTranslator(HTMLTranslator): + def __init__(self, builder, *args, **kwds): + HTMLTranslator.__init__(self, builder, *args, **kwds) + builder.templates.environment.filters['get_type'] = _getType + builder.templates.environment.tests['index'] = _isIndex + self.highlightlinenothreshold = 0 + + def visit_literal(self, node): + self.body.append(self.starttag(node, 'tt', '', CLASS='docutils literal')) + self.body.append('') + + def depart_literal(self, node): + self.body.append('') + self.body.append('') + + def visit_admonition(self, node, name=''): + self.body.append(self.starttag(node, 'div', CLASS=('admonition-wrapper'))) + self.body.append('
') + self.body.append('
') + if name and name != 'seealso': + node.insert(0, nodes.title(name, customadmonitionlabels[name])) + self.set_first_last(node) + + def depart_admonition(self, node=None): + self.body.append('
\n') + + def visit_sidebar(self, node): + self.body.append(self.starttag(node, 'div', CLASS=('admonition-wrapper'))) + self.body.append('') + self.body.append('
') + self.set_first_last(node) + self.in_sidebar = 1 + + def depart_sidebar(self, node): + self.body.append('
\n') + self.in_sidebar = None + + # overriden to add a new highlight div around each block + def visit_literal_block(self, node): + if node.rawsource != node.astext(): + # most probably a parsed-literal block -- don't highlight + return BaseTranslator.visit_literal_block(self, node) + lang = self.highlightlang + linenos = node.rawsource.count('\n') >= \ + self.highlightlinenothreshold - 1 + highlight_args = node.get('highlight_args', {}) + if node.has_key('language'): + # code-block directives + lang = node['language'] + highlight_args['force'] = True + if node.has_key('linenos'): + linenos = node['linenos'] + def warner(msg): + self.builder.warn(msg, (self.builder.current_docname, node.line)) + highlighted = self.highlighter.highlight_block( + node.rawsource, lang, warn=warner, linenos=linenos, + **highlight_args) + starttag = self.starttag(node, 'div', suffix='', + CLASS='highlight-%s' % lang) + self.body.append('
' + starttag + highlighted + '
\n') + raise nodes.SkipNode + +class SensioStyle(Style): + background_color = "#000000" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#ffffff", # class 'x' + + Comment: "italic #B729D9", # class: 'c' + Comment.Single: "italic #B729D9", # class: 'c1' + Comment.Multiline: "italic #B729D9", # class: 'cm' + Comment.Preproc: "noitalic #aaa", # class: 'cp' + + Keyword: "#FF8400", # class: 'k' + Keyword.Constant: "#FF8400", # class: 'kc' + Keyword.Declaration: "#FF8400", # class: 'kd' + Keyword.Namespace: "#FF8400", # class: 'kn' + Keyword.Pseudo: "#FF8400", # class: 'kp' + Keyword.Reserved: "#FF8400", # class: 'kr' + Keyword.Type: "#FF8400", # class: 'kt' + + Operator: "#E0882F", # class: 'o' + Operator.Word: "#E0882F", # class: 'ow' - like keywords + + Punctuation: "#999999", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#ffffff", # class: 'n' + Name.Attribute: "#ffffff", # class: 'na' - to be revised + Name.Builtin: "#ffffff", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#ffffff", # class: 'nc' - to be revised + Name.Constant: "#ffffff", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "#cc0000", # class: 'ne' + Name.Function: "#ffffff", # class: 'nf' + Name.Property: "#ffffff", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#ffffff", # class: 'nn' - to be revised + Name.Other: "#ffffff", # class: 'nx' + Name.Tag: "#cccccc", # class: 'nt' - like a keyword + Name.Variable: "#ffffff", # class: 'nv' - to be revised + Name.Variable.Class: "#ffffff", # class: 'vc' - to be revised + Name.Variable.Global: "#ffffff", # class: 'vg' - to be revised + Name.Variable.Instance: "#ffffff", # class: 'vi' - to be revised + + Number: "#1299DA", # class: 'm' + + Literal: "#ffffff", # class: 'l' + Literal.Date: "#ffffff", # class: 'ld' + + String: "#56DB3A", # class: 's' + String.Backtick: "#56DB3A", # class: 'sb' + String.Char: "#56DB3A", # class: 'sc' + String.Doc: "italic #B729D9", # class: 'sd' - like a comment + String.Double: "#56DB3A", # class: 's2' + String.Escape: "#56DB3A", # class: 'se' + String.Heredoc: "#56DB3A", # class: 'sh' + String.Interpol: "#56DB3A", # class: 'si' + String.Other: "#56DB3A", # class: 'sx' + String.Regex: "#56DB3A", # class: 'sr' + String.Single: "#56DB3A", # class: 's1' + String.Symbol: "#56DB3A", # class: 'ss' + + Generic: "#ffffff", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #ffffff", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "#000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #ffffff", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } + +def setup(app): + app.set_translator('html', SensioHTMLTranslator) diff --git a/_build/_theme/_exts/symfonycom/sphinx/lexer.py b/_build/_theme/_exts/symfonycom/sphinx/lexer.py new file mode 100644 index 00000000000..4100b66d283 --- /dev/null +++ b/_build/_theme/_exts/symfonycom/sphinx/lexer.py @@ -0,0 +1,23 @@ +from pygments.lexer import RegexLexer, bygroups, using +from pygments.token import * +from pygments.lexers.shell import BashLexer, BatchLexer + +class TerminalLexer(RegexLexer): + name = 'Terminal' + aliases = ['terminal'] + filenames = [] + + tokens = { + 'root': [ + ('^\$', Generic.Prompt, 'bash-prompt'), + ('^[^\n>]+>', Generic.Prompt, 'dos-prompt'), + ('^#.+$', Comment.Single), + ('^.+$', Generic.Output), + ], + 'bash-prompt': [ + ('(.+)$', bygroups(using(BashLexer)), '#pop') + ], + 'dos-prompt': [ + ('(.+)$', bygroups(using(BatchLexer)), '#pop') + ], + } diff --git a/_build/_theme/_templates/globaltoc.html b/_build/_theme/_templates/globaltoc.html new file mode 100644 index 00000000000..bd67cbca28d --- /dev/null +++ b/_build/_theme/_templates/globaltoc.html @@ -0,0 +1,19 @@ + diff --git a/_build/_theme/_templates/layout.html b/_build/_theme/_templates/layout.html new file mode 100644 index 00000000000..52715686172 --- /dev/null +++ b/_build/_theme/_templates/layout.html @@ -0,0 +1,100 @@ +{% extends '!layout.html' %} + +{% set css_files = ['./assets/css/app.css', './assets/css/doc.css'] %} +{# make sure the Sphinx stylesheet isn't loaded #} +{% set style = '' %} +{% set isIndex = pagename is index %} + +{% block extrahead %} +{# add JS to support tabs #} + + + + +{# pygment's styles are still loaded, undo some unwanted styles #} + +{% endblock %} + +{% block header %} +{# ugly way, now we have 2 body tags, but styles rely on these classes #} + +{% endblock %} + +{% block content %} +
+
+ {%- if render_sidebar %} + + {%- endif %} + +
+ + +

{{ title }}

+ +
+ {% block body %}{% endblock %} +
+ + {% if prev and next %} + + {% endif %} + +
+

This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.

+
+
+
+
+{% endblock %} + +{# relbar1 is at the top and should not render the quick navigation #} +{% block relbar1 %}{% endblock %} +{% block relbar2 %}{% endblock %} + +{# remove "generated by sphinx" footer #} +{% block footer %}{% endblock %} diff --git a/_build/_theme/_templates/localtoc.html b/_build/_theme/_templates/localtoc.html new file mode 100644 index 00000000000..0ffea6e1ecd --- /dev/null +++ b/_build/_theme/_templates/localtoc.html @@ -0,0 +1,6 @@ +
+

{{ _('Table Of Contents') }}

+
+ {{ toc }} +
+
diff --git a/_build/_theme/assets/css/app.css b/_build/_theme/assets/css/app.css new file mode 100644 index 00000000000..81bc0f8e391 --- /dev/null +++ b/_build/_theme/assets/css/app.css @@ -0,0 +1,3 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0}td,th{padding:0}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534} + +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{background:transparent!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;line-height:1.42857;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.initialism,.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857}dt{font-weight:700}dd{margin-left:0}.dl-horizontal dd:after,.dl-horizontal dd:before{content:" ";display:table}.dl-horizontal dd:after{clear:both}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container:after,.container:before{content:" ";display:table}.container:after{clear:both}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container-fluid:after,.container-fluid:before{content:" ";display:table}.container-fluid:after{clear:both}.row{margin-left:-15px;margin-right:-15px}.row:after,.row:before{content:" ";display:table}.row:after{clear:both}.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-1{width:8.33333%}.col-xs-2{width:16.66667%}.col-xs-3{width:25%}.col-xs-4{width:33.33333%}.col-xs-5{width:41.66667%}.col-xs-6{width:50%}.col-xs-7{width:58.33333%}.col-xs-8{width:66.66667%}.col-xs-9{width:75%}.col-xs-10{width:83.33333%}.col-xs-11{width:91.66667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.33333%}.col-xs-pull-2{right:16.66667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.33333%}.col-xs-pull-5{right:41.66667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.33333%}.col-xs-pull-8{right:66.66667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.33333%}.col-xs-pull-11{right:91.66667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.33333%}.col-xs-push-2{left:16.66667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.33333%}.col-xs-push-5{left:41.66667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.33333%}.col-xs-push-8{left:66.66667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.33333%}.col-xs-push-11{left:91.66667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0}.col-xs-offset-1{margin-left:8.33333%}.col-xs-offset-2{margin-left:16.66667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.33333%}.col-xs-offset-5{margin-left:41.66667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.33333%}.col-xs-offset-8{margin-left:66.66667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.33333%}.col-xs-offset-11{margin-left:91.66667%}.col-xs-offset-12{margin-left:100%}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-1{width:8.33333%}.col-sm-2{width:16.66667%}.col-sm-3{width:25%}.col-sm-4{width:33.33333%}.col-sm-5{width:41.66667%}.col-sm-6{width:50%}.col-sm-7{width:58.33333%}.col-sm-8{width:66.66667%}.col-sm-9{width:75%}.col-sm-10{width:83.33333%}.col-sm-11{width:91.66667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.33333%}.col-sm-pull-2{right:16.66667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333%}.col-sm-pull-5{right:41.66667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.33333%}.col-sm-pull-8{right:66.66667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333%}.col-sm-pull-11{right:91.66667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.33333%}.col-sm-push-2{left:16.66667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333%}.col-sm-push-5{left:41.66667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.33333%}.col-sm-push-8{left:66.66667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333%}.col-sm-push-11{left:91.66667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-offset-12{margin-left:100%}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-1{width:8.33333%}.col-md-2{width:16.66667%}.col-md-3{width:25%}.col-md-4{width:33.33333%}.col-md-5{width:41.66667%}.col-md-6{width:50%}.col-md-7{width:58.33333%}.col-md-8{width:66.66667%}.col-md-9{width:75%}.col-md-10{width:83.33333%}.col-md-11{width:91.66667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.33333%}.col-md-pull-2{right:16.66667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333%}.col-md-pull-5{right:41.66667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.33333%}.col-md-pull-8{right:66.66667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333%}.col-md-pull-11{right:91.66667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.33333%}.col-md-push-2{left:16.66667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333%}.col-md-push-5{left:41.66667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.33333%}.col-md-push-8{left:66.66667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333%}.col-md-push-11{left:91.66667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.33333%}.col-md-offset-2{margin-left:16.66667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333%}.col-md-offset-5{margin-left:41.66667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333%}.col-md-offset-8{margin-left:66.66667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333%}.col-md-offset-11{margin-left:91.66667%}.col-md-offset-12{margin-left:100%}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-1{width:8.33333%}.col-lg-2{width:16.66667%}.col-lg-3{width:25%}.col-lg-4{width:33.33333%}.col-lg-5{width:41.66667%}.col-lg-6{width:50%}.col-lg-7{width:58.33333%}.col-lg-8{width:66.66667%}.col-lg-9{width:75%}.col-lg-10{width:83.33333%}.col-lg-11{width:91.66667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.33333%}.col-lg-pull-2{right:16.66667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333%}.col-lg-pull-5{right:41.66667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.33333%}.col-lg-pull-8{right:66.66667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333%}.col-lg-pull-11{right:91.66667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.33333%}.col-lg-push-2{left:16.66667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333%}.col-lg-push-5{left:41.66667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.33333%}.col-lg-push-8{left:66.66667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333%}.col-lg-push-11{left:91.66667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.33333%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-offset-12{margin-left:100%}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777}caption,th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{margin:0;min-width:0}fieldset,legend{padding:0;border:0}legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{padding-top:7px}.form-control,output{display:block;font-size:14px;line-height:1.42857;color:#555}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control:focus{border-color:#66afe9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox-inline input[type=checkbox],.checkbox input[type=checkbox],.radio-inline input[type=radio],.radio input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .checkbox label,fieldset[disabled] .radio-inline,fieldset[disabled] .radio label,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.33333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success.checkbox-inline label,.has-success.checkbox label,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.radio-inline label,.has-success.radio label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning.checkbox-inline label,.has-warning.checkbox label,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.radio-inline label,.has-warning.radio label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error.checkbox-inline label,.has-error.checkbox label,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.radio-inline label,.has-error.radio label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:after,.form-horizontal .form-group:before{content:" ";display:table}.form-horizontal .form-group:after{clear:both}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#337ab7}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\A0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.modal,.modal-open{overflow:hidden}.modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translateY(-25%);-ms-transform:translateY(-25%);-o-transform:translateY(-25%);transform:translateY(-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0);-ms-transform:translate(0);-o-transform:translate(0);transform:translate(0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header:after,.modal-header:before{content:" ";display:table}.modal-header:after{clear:both}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:after,.modal-footer:before{content:" ";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.clearfix:after,.clearfix:before{content:" ";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}.inheritParentStyles,h1 code,h2 code,h3 code,h4 code,h5 code,h6 code{background:inherit;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit}body{color:#18171b;font-family:Lucida Grande,Lucida Sans Unicode,Lucida Sans,Geneva,Verdana,sans-serif;font-size:14px;line-height:1.45}blockquote{font-size:1em;font-style:italic;margin:0 0 15px;padding:5px 15px}.xs-separator{border:0;height:0;margin:30px 0 0;padding:0}.block{clear:both;display:block}.nowrap{white-space:nowrap}.link{color:#006dcb!important;text-decoration:none}.link:hover{color:#0088fe!important;text-decoration:underline}.text-hero{font-size:32px;text-align:center}.text-bold{font-weight:700!important}.text-small{font-size:12px}.text-large{font-size:16px}.text-accent{color:#4d8400;font-style:italic}.text-muted{color:#767676}.text-muted a{color:inherit}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.font-system{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif!important}.font-base{font-family:Lucida Grande,Lucida Sans Unicode,Lucida Sans,Geneva,Verdana,sans-serif!important}.disabled,.disabled a{color:#d9d9d9}.disabled a{text-decoration:underline}.m-b-5{margin-bottom:5px!important}.m-b-15{margin-bottom:15px!important}.m-b-30{margin-bottom:30px!important}.m-t-5{margin-top:5px!important}.m-t-15{margin-top:15px!important}.m-t-30{margin-top:30px!important}.m-r-5{margin-right:5px!important}.m-r-15{margin-right:15px!important}.m-b-0{margin-bottom:0!important}.m-t-0{margin-top:0!important}.p-15{padding:15px!important}.columns--two{column-count:2;column-gap:35px}.columns--three{column-count:3;column-gap:35px}a:active,a:focus,a:hover{outline:0}a{color:#006dcb;text-decoration:none}a:hover{color:#0088fe;text-decoration:underline}a.read-more{font-weight:700}a.read-more:after{content:"\A0\2192"}.rss a{color:#ff854f;font-weight:700}dl,ol,ul{margin:0 0 15px 30px;padding:0}dl dl,ol ol,ol ul,ul ol,ul ul{margin-bottom:15px;margin-top:7px}li+li{margin-top:8px}dl{margin-left:0}dt{font-weight:400;margin-bottom:5px}dd{margin-left:15px}dd+dt{margin-top:15px}.list--unstyled{list-style-type:none;margin-left:0;padding:0}.list--numbered{list-style-type:decimal}.list--borderless li{border:none!important}.list--border li+li{border-top:1px solid #f5f5f5;padding-top:8px}.h1,h1{font-size:24px}.h2,h2{font-size:21px}.h3,.h4,h3,h4{font-size:18px}.h5,.h6,h5,h6{font-size:14px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:Georgia,Times New Roman,Times,serif;font-weight:400;line-height:1.2;margin-top:0;margin-bottom:.5em}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color:#18171b}h1 a:hover,h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,h6 a:hover{color:#006dcb;text-decoration:none}.headerlink{color:#d9d9d9;font-size:80%;font-weight:400;padding-left:5px}.headerlink:hover,h1:hover .headerlink,h2:hover .headerlink,h3:hover .headerlink,h4:hover .headerlink,h5:hover .headerlink,h6:hover .headerlink{color:#006dcb}p{margin:0 0 15px}p.lead{color:#63606b;font-family:Georgia,Times New Roman,Times,serif;font-style:italic;font-size:18px;margin:30px auto;text-align:center;width:94%}img{max-width:100%}.rounded{border-radius:5px}.screenshot{border:1px solid #d9d9d9;box-shadow:1px 1px 5px #c0bec3;display:block;margin:15px auto;max-height:1024px;max-width:90%;padding:5px}.avatar{border:1px solid #f5f5f5;border-radius:50%;height:24px;margin:0;width:24px}.avatar--large{height:40px;width:40px}.avatar--extra-large{height:100px;width:100px}.with-browser{border:solid #d9d9d9;border-width:35px 3px 3px;border-radius:3px 3px 0 0;margin-bottom:15px;position:relative}.with-browser:before{background-color:#f44;border-radius:50%;box-shadow:0 0 0 2px #f44,1.5em 0 0 2px #fb5,3em 0 0 2px #9b3;height:.5em;left:1em;opacity:.4;top:-1.5em;width:.5em}.with-browser:after,.with-browser:before{content:"";display:block;position:absolute}.with-browser:after{background-color:#fafafa;border-radius:2px;height:20px;right:3px;top:-27px;width:calc(100% - 6em)}.with-browser img{border:1px solid #ccc;border-radius:0;margin:0!important}.post__content img,main .section img{margin-bottom:15px}table{border:1px double #c0bec3;border-collapse:collapse;margin:1.5em 0;max-width:100%;width:100%}.table--separated{border-collapse:separate;border-spacing:15px 0;border:0}.table--center td,.table--center th{text-align:center;vertical-align:middle}thead{border-bottom:1px double #c0bec3}td,th{border:1px solid #d9d9d9;padding:.5em;text-align:left;vertical-align:middle}th{background:#f5f5f5}.modal{z-index:10099}label.required:after{content:"*";color:red}.form-control{color:#18171b}.form-control:focus{border-color:#999;box-shadow:0 0 0 4px #eee;outline:0}.has-error .form-control:focus{border-color:#a94442;box-shadow:0 0 0 4px #f2dede}.textarea--small{min-height:65px}.textarea--medium{min-height:150px}.textarea--large{min-height:350px}.textarea--code{font-family:Droid Sans Mono,Consolas,Menlo,Ubuntu Mono,monospace;font-size:13px}select+select{margin-left:5px}.pre select,pre select{color:#18171b}.form--error{background:#f2dede;color:#a94442;margin:1em 0;padding:10px 15px}.form--help{color:#999;display:block;margin:5px 0 10px}.help-block ul{list-style:none;margin:5px 0}.btn,.btn:focus,.btn:hover{background:#4d8400;border:0;box-shadow:0 3px #2f5100;border-radius:5px;color:#fff;cursor:pointer;display:inline-block;font-size:14px;outline:none;padding:6px 12px;position:relative;text-align:center;text-decoration:none;touch-action:manipulation;vertical-align:middle;white-space:nowrap}.btn:hover{opacity:.9}.btn:active{box-shadow:0 1px #2f5100;top:2px}.btn+.btn{margin-left:1em}.btn--danger,.btn--danger:focus,.btn--danger:hover{background-color:#c00;box-shadow:0 3px #900}.btn--danger:active{box-shadow:0 1px #900}.btn--large,.btn--large:focus,.btn--large:hover{font-size:16px}.btn--copy,.btn--copy:focus,.btn--copy:hover,.pre .btn--copy,pre .btn--copy{background-image:-webkit-linear-gradient(top,#eee,#ccc);background-image:-o-linear-gradient(top,#eee 0,#ccc 100%);background-image:linear-gradient(180deg,#eee 0,#ccc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#FFEEEEEE",endColorstr="#FFCCCCCC",GradientType=0);box-shadow:0 3px #777;color:#18171b;padding:2px 8px;text-transform:uppercase}.btn--copy:focus i,.btn--copy:hover i,.btn--copy i,.pre .btn--copy i,pre .btn--copy i{display:inline-block;margin-bottom:4px;vertical-align:top}.btn--copy:focus span,.btn--copy:hover span,.btn--copy span,.pre .btn--copy span,pre .btn--copy span{color:#18171b;display:inline-block;padding-top:5px}.btn--copy:active{box-shadow:0 1px #777}form .btn{position:relative;top:-2px}form .btn:active{top:0}.badge--outline{background:transparent;border:2px solid #d9d9d9;border-radius:4px;color:#63606b}code,pre{border:0;font-family:Droid Sans Mono,Consolas,Menlo,Ubuntu Mono,monospace;font-size:13px;font-variant-ligatures:no-common-ligatures;text-rendering:optimizeSpeed}code{background:#f5f5f5;border-radius:3px;color:#18171b;padding:2px 3px;white-space:pre-wrap;word-wrap:break-word;word-break:break-word}.pre,pre{background-color:#18171b;color:#fff;line-height:1.6;margin:15px 0;padding:15px}.pre--copy{position:relative}.pre--copy .btn--copy{position:absolute;top:8px;right:5px}.pre--copy .btn--copy:active{top:10px}.literal-block{margin:0 0 15px}.literal-block pre{margin:0}.highlighttable{border:0;margin:0;table-layout:fixed}.highlighttable td.code{border:0;padding:0}.highlighttable td.linenos{background-color:#18171b;border:0;border-right:1px solid #222;padding:15px 5px;text-align:right;vertical-align:top;width:35px}.highlighttable td.linenos pre{color:#63606b;padding:0}.bp{color:#3465a4}.c,.c1,.cm,.cs{color:#b729d9;font-style:italic}.cp{color:#a0a0a0}.err{color:#a40000;border:1px solid #ef2929}.g{color:#fff}.gd{color:#a40000}.ge{color:#fff;font-style:italic}.gh{color:navy}.gi{color:#00a000}.go{color:gray}.gp{color:#745334}.gr{color:#ef2929}.gs{color:#fff}.gs,.gt{font-weight:700}.gt{color:#a40000}.gu{color:purple;font-weight:700}.hll{background-color:#ff3}.il{color:#1299da}.k,.kc,.kd,.kn,.kp,.kr,.kt{color:#ff8400}.l,.ld{color:#fff}.m,.mf,.mh,.mi,.mo{color:#1299da}.n,.na,.nb,.nc,.nf,.nn,.no,.nv,.nx{color:#fff}.nd{color:gray}.ne{color:#ef2929}.nl{color:#ff8400}.nt{color:#ccc}.ni,.o,.ow{color:#e67700}.p{color:#939393}.py{color:#fff}.s,.s1,.s2,.sb,.sc,.se,.sh,.si,.sr,.ss,.sx{color:#56db3a}.sd{color:#b729d9;font-style:italic}.vc,.vg,.vi{color:#fff}.w{color:#f8f8f8;text-decoration:underline}.x{color:#fff}.p-Indicator{color:#ff8400}.highlight-rst .gh{color:#fff}.highlight-php .highlight .err{border:0;color:inherit}.highlight-diff .highlight>pre{padding-left:1.5em}.highlight-diff .gi{background:rgba(51,102,102,.4);color:#8c8;margin-left:-15px;padding:2px 0 2px 1px}.highlight-diff .gd{background:rgba(102,51,51,.6);color:#c88;margin-left:-15px;padding:2px 0 2px 1px}.highlight-bash,.highlight-terminal{border:solid #555;border-width:30px 3px 4px 4px;border-radius:3px 3px 0 0;position:relative}.highlight-bash:before,.highlight-terminal:before{background-color:#777;border-radius:50%;box-shadow:0 0 0 2px #777,1.5em 0 0 2px #777,3em 0 0 2px #777;content:"";display:block;height:.5em;left:1em;position:absolute;top:-1.25em;width:.5em}.highlight-bash td.linenos,.highlight-terminal td.linenos{display:none}.highlight-bash .highlighttable,.highlight-terminal .highlighttable{outline:1px solid #555}.highlight-bash .pre,.highlight-bash pre,.highlight-terminal .pre,.highlight-terminal pre{margin:0}.highlight-bash span,.highlight-terminal span{border:none;color:#fff}.highlight-bash .c,.highlight-bash .c1,.highlight-bash .cm,.highlight-bash .cs,.highlight-terminal .c,.highlight-terminal .c1,.highlight-terminal .cm,.highlight-terminal .cs{color:#b729d9;font-style:italic}.highlight-bash .s,.highlight-bash .s1,.highlight-bash .s2,.highlight-terminal .s,.highlight-terminal .s1,.highlight-terminal .s2{color:#56db3a}.highlight-bash .gp,.highlight-terminal .gp{color:gray}.highlight-terminal .c1,.highlight-terminal .gp{display:inline-block;position:relative;visibility:hidden}.highlight-terminal .c1:before,.highlight-terminal .gp:before{content:attr(data-content);position:absolute;visibility:visible}.container{max-width:970px;width:100%}section{margin-bottom:45px}p+section{margin-top:45px}main{padding-bottom:30px}aside{background:#f5f5f5;margin-bottom:15px;padding:15px}aside h3{font-size:18px}header{z-index:9999}header a:focus,header a:hover{text-decoration:none}header section{margin-bottom:0}.header__bottom .container,.header__top .container{display:flex;align-content:space-between;align-items:center}.header__top{background:#222;flex:1;padding:6px 0}.header__logo{flex:1}.header__logo img{height:25px;filter:invert(100%)}.header__logo--responsive img{height:30px}.header__bottom{transition:all .3s ease 0s}.header__bottom .header__logo--responsive{display:none}.header__bottom.affix{box-shadow:0 0 20px 0 rgba(46,40,64,.5);width:100%;top:36px;z-index:9999}.header__bottom.affix .header__logo--responsive{display:block}.header__nav{background:#53585f;display:none;flex:1;position:absolute;width:100%;margin-left:-15px;top:84px;z-index:9999}.header__nav ul{display:flex;flex-direction:column;align-content:space-between;margin:0}.header__nav li{list-style-type:none;flex:1 1 auto;margin:0;padding-left:15px}.header__nav li:hover{background-color:rgba(0,0,0,.1)}.header__nav li.selected,.header__nav li.selected:hover{background:rgba(0,0,0,.3)}.header__nav a{color:#d9d9d9;display:block;height:40px;line-height:40px}.header__nav li.selected a{color:#4d8400}.header__download a{background:#7aba20;color:#fff;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13px;font-weight:700;line-height:40px;text-align:center;text-transform:uppercase;transition:opacity .2s ease-in-out 0s}.header__download a:hover{opacity:.8}.header__toggle__menu i svg{padding-top:4px}.header__toggle__menu,.header__toggle__menu:focus,.header__toggle__menu:hover{background:transparent;border:none;outline:none;text-decoration:none;transform:rotate(0deg);transition:all .2s ease 0s}.header__toggle__menu.open{display:inline-block;transform:rotate(-90deg)}footer{background-color:#18171b;color:#c0bec3}footer section{margin-bottom:0;padding:45px 0 0}footer h6,footer h6 a{color:#d9d9d9;font-family:Lucida Grande,Lucida Sans Unicode,Lucida Sans,Geneva,Verdana,sans-serif;font-weight:700;font-size:12px;margin:0 0 5px}footer a,footer a:hover{color:#c0bec3;text-decoration:none}footer a:hover{text-decoration:underline}footer .metadata{color:#a7a7a7;margin-top:2px}footer .icon__group{margin:30px 0}footer .icon__group .icon{margin:5px 15px 5px 0}footer .icon__group path{fill:#999}footer .icon__group a:hover{text-decoration:none}footer .icon__group a:hover path{fill:#ccc}footer .promos{background-color:#f5f5f5;color:#63606b}footer .promos .list--unstyled a{color:#18171b}footer .promos .list--unstyled a:hover,footer .promos a,footer .promos a:hover{color:#006dcb}.sitemap{display:flex;flex-wrap:wrap;font-size:11px;justify-content:space-between;list-style:none;margin:0}.sitemap ul{margin:0}.sitemap>li{width:50%;margin-bottom:15px}.sitemap>li li{margin:0 0 5px}.sitemap--large{font-size:14px}.sitemap--large h6{font-size:18px}.sitemap--large>li{width:100%}.sitemap--large>li li{margin:0 0 5px 15px}.metadata{color:#63606b;font-size:13px;margin-bottom:7px}.metadata--sidebar .section{margin-bottom:30px}.metadata--sidebar h4{color:#4d8400;font-family:Lucida Grande,Lucida Sans Unicode,Lucida Sans,Geneva,Verdana,sans-serif;font-size:12px;font-weight:700}.metadata--sidebar p{margin-bottom:.5em}.metadata--sidebar ul{margin:.5em 0 0 1.5em}.icon svg{height:24px;width:24px;vertical-align:text-top}.icon--small svg{height:16px;width:16px}.icon--large svg{height:32px;width:32px}.icon--rss path{fill:#ff854f}.icon--white path{fill:#fff}.icon--dark-gray path{fill:#63606b}.icon--gray path{fill:#d9d9d9}.calendar--yearly{line-height:2;margin-bottom:15px}.calendar--yearly .year{font-weight:700;margin-bottom:0}.calendar--yearly .disabled{color:#c0bec3}.calendar--yearly a{color:#18171b;text-decoration:none}.calendar--yearly a:hover{color:#006dcb;text-decoration:underline}.calendar--daily{outline:2px solid #ededed;text-align:center;width:57px}.calendar--daily .month{background-color:#ed2939;color:#fff;display:block;font-size:12px;padding:2px 0;text-transform:uppercase}.calendar--daily .day{border:1px solid #ccc;border-top:0;display:block;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:24px!important;padding:0}.metrics{display:flex;flex-direction:column;justify-content:space-around;margin:15px 0}.metric{padding:0 15px;text-align:center}.metric+.metric{margin-top:30px}.metric__value{color:#63606b;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-feature-settings:tnum;font-size:32px;font-variant-numeric:tabular-nums}.metric__label{display:block;font-size:12px}.breadcrumb{background-color:transparent;border-radius:0;font-size:12px;margin:0 0 15px;padding:0}.breadcrumb>li{margin:0}.breadcrumb>li+li:before{padding:0}.breadcrumb>li.active,.breadcrumb>li.active a,.breadcrumb>li.active a:hover{color:#c0bec3;cursor:default;text-decoration:none}.breadcrumb li.active{display:none}.pager{display:flex;list-style:none;margin:15px 0}.pager li{flex:1}.pager li>a,.pager li>span{border:none}.pager li>a{font-weight:700;padding:5px 0}.pager li>a:hover{background:transparent}.pager li.disabled{color:#777;padding:5px 0}.box,.metrics{-webkit-column-break-inside:avoid;-moz-column-break-inside:avoid;column-break-inside:avoid;background:#f5f5f5;border-radius:10px;margin:15px 0;padding:15px}.box>:last-child,.box>:last-child :last-child,.metrics>:last-child,.metrics>:last-child :last-child{margin-bottom:0}.box--small,.metrics{padding:15px}.box--shadow,.metrics{background:none;box-shadow:0 1px 6px rgba(0,0,0,.2)}.box--outline{border:2px solid #d9d9d9;background:none}.box--dashed{border:2px dashed #d9d9d9;background:none}.box--warning{background:#fffccc;border:1px solid rgba(0,0,0,.1)}.box--error{background:#f2dede;border:1px solid #ebccd1;color:#a94442}.submenu__nav{display:none;list-style:none;margin:0;padding:0}.submenu__nav li{margin-top:0}.submenu__nav li+li{border-top:1px solid #f5f5f5}.submenu__nav li:first-child a{padding-top:0}.submenu__nav li.selected a{font-weight:700}.submenu__nav a{color:#18171b;display:block;padding:7px 0}.submenu__nav a:hover{color:#006dcb;text-decoration:none}.ads{display:none;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:12px}.ads h3{margin:0;font:inherit!important;font-size:14px!important}.ads h3 a{color:#006dcb}.ads cite{color:#4d8400}.tinynav_label,footer .tinynav{display:none}@media (min-width:768px){.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:21px}.h4,h4{font-size:18px}.h5,.h6,h5,h6{font-size:14px}.xs-separator{display:none}.text-hero{font-size:36px}#sln{background:#2f2f2f;height:36px;position:fixed;top:0;width:100%;z-index:10000}header{margin-bottom:25px}#content_wrapper{margin-top:36px}aside{background:transparent;padding-top:0}aside .tinynav{display:none}.submenu{margin-bottom:30px}.ads,.submenu__nav{display:block}.header__top{background:#f0f0f0;padding:20px 0}.header__bottom{background:#f9f9f9}.header__top .container{padding-right:15px}.header__logo img{height:50px;filter:invert(0)}.header__nav{background:transparent;display:block!important;position:static;width:100%;margin-left:0}.header__nav ul{flex-direction:row;flex-wrap:wrap;font-size:12px;height:40px;overflow:hidden}.header__nav li{padding-left:0}.header__nav li.selected,.header__nav li.selected:hover,.header__nav li:hover{background:transparent}.header__nav li.selected a{color:#006dcb;font-weight:700}.header__nav a{color:#18171b;display:inline}.header__download a{color:#fff;display:block}.sitemap>li{width:auto}.sitemap--large>li{margin-bottom:30px;width:33%}.sitemap--large>li+li,.sitemap>li+li{margin-top:0}.sitemap--large>li li{margin-left:0}.metrics{flex-direction:row}.metric+.metric{margin-top:0}.breadcrumb li.active{display:inline-block}p.lead{font-size:21px;width:90%}.box,.metrics{padding:30px}.box--small,.metrics{padding:15px}}@media (min-width:992px){.header__nav ul{font-size:14px}} \ No newline at end of file diff --git a/_build/_theme/assets/css/doc.css b/_build/_theme/assets/css/doc.css new file mode 100644 index 00000000000..b45e348b011 --- /dev/null +++ b/_build/_theme/assets/css/doc.css @@ -0,0 +1 @@ +.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-body:after,.panel-body:before{content:" ";display:table}.panel-body:after{clear:both}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle,.panel-title{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table-responsive>.table caption,.panel>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.admonition-sidebar .sidebar-title code,.doc__toc code,.inheritParentStyles,.toctree-wrapper code{background:inherit;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit}.section+.section,.section>.section{margin-top:30px}.section>:last-child{margin-bottom:0;padding-bottom:0}.section h1{display:none}.section h4,.section h5,.section h6{font-size:19px}.section li a em,.toctree-wrapper em{font-style:normal}.configuration-block{margin:15px 0;position:relative}.configuration-block ul.simple{margin:0;list-style:none}.configuration-block li{display:inline-block;margin:0 5px 0 0}.configuration-block li em{font-style:normal}.configuration-block li a{background-color:#d9d9d9;border-top-left-radius:5px;border-top-right-radius:5px;color:#18171b;display:block;padding:5px 12px 3px;text-decoration:none}.configuration-block li a:hover{background-color:#c0bec3;color:#18171b}.configuration-block .selected a,.configuration-block .selected a:hover{background-color:#18171b;color:#fff;text-decoration:none}.configuration-block .literal-block{margin:-1px 0 0;position:absolute;left:0}.admonition-wrapper code,.versionadded code{background:hsla(0,0%,100%,.1)}.admonition-wrapper{margin:0 0 15px;position:relative}.admonition-wrapper .admonition-wrapper{border:none}.admonition{border-radius:10px;padding:15px}.admonition-title{background-position:0 50%;background-repeat:no-repeat;background-size:21px;font-weight:700;height:21px;line-height:21px;margin-bottom:10px;padding-left:28px}.admonition .last,.admonition>:last-child{margin-bottom:0}.admonition-note{border:2px solid #d9d9d9}.admonition-note .admonition-title{background-image:url(/images/v5/doc/note.svg);color:#999}.admonition-tip{border:2px solid rgba(170,205,78,.5)}.admonition-tip .admonition-title{background-image:url(/images/v5/doc/tip.svg);color:#aacd4e}.admonition-caution{border:2px solid rgba(217,83,79,.3)}.admonition-caution .admonition-title{background-image:url(/images/v5/doc/caution.svg);color:#d9534f}.admonition-sidebar{background:#fafafa;border:2px solid #d9d9d9}.admonition-sidebar .sidebar-title{margin:0 0 5px;font-family:Georgia,Times New Roman,Times,serif;font-size:21px}.admonition.admonition-best-practice{border:2px solid rgba(223,154,7,.3)}.admonition.admonition-best-practice .admonition-title.first{display:none}.admonition.admonition-best-practice .admonition-title{background-image:url(/images/v5/doc/bestpractice.svg);color:#df9a07}.admonition-seealso{border:2px solid #d9d9d9}.versionadded{background:rgba(0,136,204,.05);border:2px solid rgba(0,136,204,.2);border-radius:10px;margin:15px 0;padding:15px!important}.versionadded p{margin-bottom:0}.versionadded .versionmodified{color:#006dcb;float:left;font-weight:700;margin-right:5px}a.footnote-reference{font-size:12px;line-height:.5;vertical-align:top}.footnote,.footnote>tbody>tr{border:none}.footnote>tbody>tr>td{border:none;padding-bottom:1em;padding-top:0}.footnote>tbody>tr>td.label{color:#18171b;font-size:14px;font-weight:700}.footnote>tbody>tr>td.label a.fn-backref{color:#18171b}.footnote>tbody>tr>td>em:first-child{display:none}.doc__tools{font-size:12px;margin:15px 0;position:relative;z-index:100}.doc__version{float:left;margin-right:15px;min-height:24px;position:relative;width:120px}.doc__version:hover .version__all{display:block}.doc__version:hover .version__current{border-radius:0;border-top-left-radius:4px;border-top-right-radius:4px}.version__current{background-color:#63606b;border-radius:4px;color:#f5f5f5;cursor:pointer;padding:3px 7px;position:relative;z-index:9899}.version__current:hover{border-radius:4px 4px 0 0;border-bottom-left-radius:0;border-bottom-right-radius:0}.version__all{background-color:#63606b;border-radius:4px;border-top-left-radius:0;box-shadow:0 0 0 5px #fff;display:none;margin-top:-2px;padding:10px 7px;position:absolute;left:0;width:225px}.version__all strong{color:#d1e9fa}.version__all small{color:#c0bec3;display:block}.version__all span{color:#c0bec3;padding:10px}.version__all--narrow{width:100%}.version__all--narrow strong{display:none}.version__all--narrow .version__list{display:block}.version__all--narrow .version__list a,.version__all--narrow .version__list a:hover{display:block;padding:5px 0 10px;text-align:left}.version__all--narrow .version__list a:hover:last-child,.version__all--narrow .version__list a:last-child{padding-bottom:0}.version__all--narrow .version__list small{display:inline;font-size:inherit}.version__all--narrow .version__list small:before{content:" / "}.version__list{display:flex;flex-wrap:wrap}.version__list a,.version__list a:hover{color:#f5f5f5;line-height:1;padding:10px 7px;text-align:center}.doc__edit{background-color:#006dcb;border-radius:4px;float:left;text-align:center;width:120px}.doc__edit:hover{opacity:.9}.doc__edit a,.doc__edit a:hover{color:#fff;display:block;min-height:24px;padding:3px 7px}.doc__nav .panel{border-bottom:1px solid #f5f5f5;box-shadow:none;margin:0}.doc__nav a,.doc__nav a:focus{color:#18171b;display:block;text-decoration:none}.doc__nav a:hover{color:#006dcb;text-decoration:none}.doc__nav .panel:first-child .panel-heading a{padding-top:0}.doc__nav .panel-heading{padding:0}.doc__nav .panel-heading a,.doc__nav .panel-heading a:focus{font-weight:700;padding:7px 0}.doc__nav .panel-heading a.collapsed{font-weight:400}.doc__nav .panel-heading a:not(.collapsed) .icon{transform:rotate(90deg)}.doc__nav .panel-body{padding:0}.doc__nav .panel-body ul{color:#d9d9d9;list-style:disc;margin:0 0 15px 25px}.doc__nav .panel-body li{margin:0 0 5px}.doc__nav .panel-body li.selected a{font-weight:700}.doc__nav .panel-body a{font-size:13px;padding:0}.doc__toc{max-height:300px;overflow-y:auto}.doc__toc .box{margin:0;padding:0}.doc__toc ul{margin-left:20px;list-style:circle}.doc__toc .box>ul{margin:0 0 0 20px}.doc__toc .box>ul>li>ul{margin:0}.doc__toc .box>ul>li>a{display:none}.doc__toc .box>ul>li>ul>li{list-style:disc}.tag-cloud{align-items:center;display:flex;flex-wrap:wrap;justify-content:center;list-style:none;margin:0}.tag-cloud li,.tag-cloud li+li{margin:0}.tag-cloud a{display:inline-block;padding:5px 10px 10px}.tag-cloud a.important{font-size:18px}.tag-cloud a.very-important{font-size:18px;font-weight:700}@media (min-width:768px){.doc__tools{background:#fff;float:right;margin:5px 0 15px 15px;padding:0 0 5px 5px;width:130px}.doc__version{float:none;margin-bottom:5px;margin-right:0;width:100%}.version__all{border-top-left-radius:4px;border-top-right-radius:0;left:auto;right:0}.doc__edit{float:none;text-align:left;width:100%}.doc__toc{max-height:none;overflow:hidden}.doc__toc .box{margin-bottom:30px;padding:15px}.doc__toc li,.doc__toc ul{list-style:none!important}.doc__toc .box>ul{line-height:1.2;margin:0}.doc__toc .box>ul ul{margin-bottom:10px;margin-left:10px}.doc__toc .box>ul ul ul li{margin-top:5px}.doc__toc a{color:#63606b;font-size:12px;font-weight:700}.doc__toc li li li a{font-weight:400}.admonition{padding:15px 15px 15px 55px}.admonition-title,.seealso{background-position:50% 0;background-repeat:no-repeat;background-size:28px;font-size:12px;left:15px;line-height:1;margin-bottom:0;padding-left:0;padding-top:30px;position:absolute;text-align:center;text-indent:-9999px;text-transform:uppercase;top:13px;width:28px}.admonition-sidebar{padding:30px}.admonition-wrapper .seealso{background-image:url(/images/v5/doc/seealso.svg)}} \ No newline at end of file diff --git a/_build/_theme/assets/js/app.js b/_build/_theme/assets/js/app.js new file mode 100644 index 00000000000..3bee51c6892 --- /dev/null +++ b/_build/_theme/assets/js/app.js @@ -0,0 +1,24 @@ +webpackJsonp([2],{0:function(e,t,n){n("7t+N"),n("6J7x"),n("dXU8"),n("MVPP"),n("iuXZ"),n("mS0f"),e.exports=n("3Z84")},"3Z84":function(e,t,n){(function(e,t){e.$=e.jQuery=n("7t+N"),t(document).ready(function(){function e(e){if(t(window).width()<768)var n=10;else if(t(window).width()<970)var n=50;else var n=90;if("string"==typeof e)var i=e;else var i=t(this).attr("href")||t(this).attr("id");if(i){var r=t(i);return r.length&&(t("html, body").animate({scrollTop:r.offset().top-n}),history&&"pushState"in history)?(history.pushState({},document.title,window.location.pathname+window.location.search+i),!1):void 0}}t("#header-bottom").on("affixed.bs.affix",function(){t("body").css("padding-top","40px")}),t("#header-bottom").on("affixed-top.bs.affix",function(){t("body").css("padding-top","0")}),t("body").on("click","main a[href^='#']",e),t(".header__toggle__menu").on("click",function(e){t(this).toggleClass("open"),t(".header__nav").slideToggle(),e.preventDefault()}),t(".submenu__nav").each(function(){t(this).tinyNav(),t("select.tinynav").each(function(){t(this).addClass("form-control"),t(this).attr("title","Secondary navigation menu")})}),t(".tinynav").bind("change",function(){var e=t(this).val();return e&&(window.location=e),!1}),t("[data-tracked]").bind("click",function(e){try{ga("send","event",t(this).data("category")||"",t(this).data("action")||"",t(this).data("label")||"",t(this).data("value")||"")}catch(e){}}),t("#ad_jobs").each(function(){t.get(t(this).attr("data-url"),function(e){var n=t('

Job '+e.company+"

"+e.city+", "+e.country_name+"
"+e.title+"
jobs.sensiolabs.com
");t("#ad_jobs").replaceWith(n)},"json")})})}).call(t,n("DuR2"),n("7t+N"))},"6J7x":function(e,t,n){(function(e){!function(e,t,n){e.fn.tinyNav=function(i){var r=e.extend({active:"selected",header:"",indent:"- ",label:"",addHashHistory:!1},i);return this.each(function(){n++;var i=e(this),o="tinynav",s=o+n,a=".l_"+s,u=e("",e.querySelectorAll("[msallowcapture^='']").length&&O.push("[*^$]="+ee+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||O.push("\\["+ee+"*(?:value|"+Z+")"),e.querySelectorAll("[id~="+F+"-]").length||O.push("~="),e.querySelectorAll(":checked").length||O.push(":checked"),e.querySelectorAll("a#"+F+"+*").length||O.push(".#.+[+~]")}),r(function(e){e.innerHTML="";var t=$.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&O.push("name"+ee+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&O.push(":enabled",":disabled"),L.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&O.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),O.push(",.*:")})),(x.matchesSelector=he.test(P=L.matches||L.webkitMatchesSelector||L.mozMatchesSelector||L.oMatchesSelector||L.msMatchesSelector))&&r(function(e){x.disconnectedMatch=P.call(e,"*"),P.call(e,"[s!='']:x"),H.push("!=",ie)}),O=O.length&&new RegExp(O.join("|")),H=H.length&&new RegExp(H.join("|")),t=he.test(L.compareDocumentPosition),R=t||he.test(L.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,i=t&&t.parentNode;return e===i||!(!i||1!==i.nodeType||!(n.contains?n.contains(i):e.compareDocumentPosition&&16&e.compareDocumentPosition(i)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},z=t?function(e,t){if(e===t)return A=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&n||!x.sortDetached&&t.compareDocumentPosition(e)===n?e===$||e.ownerDocument===I&&R(I,e)?-1:t===$||t.ownerDocument===I&&R(I,t)?1:D?K(D,e)-K(D,t):0:4&n?-1:1)}:function(e,t){if(e===t)return A=!0,0;var n,i=0,r=e.parentNode,o=t.parentNode,a=[e],u=[t];if(!r||!o)return e===$?-1:t===$?1:r?-1:o?1:D?K(D,e)-K(D,t):0;if(r===o)return s(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;a[i]===u[i];)i++;return i?s(a[i],u[i]):a[i]===I?-1:u[i]===I?1:0},$):$},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==$&&j(e),n=n.replace(ue,"='$1']"),x.matchesSelector&&q&&!U[n+" "]&&(!H||!H.test(n))&&(!O||!O.test(n)))try{var i=P.call(e,n);if(i||x.disconnectedMatch||e.document&&11!==e.document.nodeType)return i}catch(e){}return t(n,$,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==$&&j(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==$&&j(e);var n=w.attrHandle[t.toLowerCase()],i=n&&X.call(w.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==i?i:x.attributes||!q?e.getAttribute(t):(i=e.getAttributeNode(t))&&i.specified?i.value:null},t.escape=function(e){return(e+"").replace(be,xe)},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],i=0,r=0;if(A=!x.detectDuplicates,D=!x.sortStable&&e.slice(0),e.sort(z),A){for(;t=e[r++];)t===e[r]&&(i=n.push(r));for(;i--;)e.splice(n[i],1)}return D=null,e},T=t.getText=function(e){var t,n="",i=0,r=e.nodeType;if(r){if(1===r||9===r||11===r){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=T(e)}else if(3===r||4===r)return e.nodeValue}else for(;t=e[i++];)n+=T(t);return n},w=t.selectors={cacheLength:50,createPseudo:i,match:fe,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(ve,ye),e[3]=(e[3]||e[4]||e[5]||"").replace(ve,ye),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return fe.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&le.test(n)&&(t=k(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(ve,ye).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=B[e+" "];return t||(t=new RegExp("(^|"+ee+")"+e+"("+ee+"|$)"))&&B(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,n,i){return function(r){var o=t.attr(r,e);return null==o?"!="===n:!n||(o+="","="===n?o===i:"!="===n?o!==i:"^="===n?i&&0===o.indexOf(i):"*="===n?i&&o.indexOf(i)>-1:"$="===n?i&&o.slice(-i.length)===i:"~="===n?(" "+o.replace(re," ")+" ").indexOf(i)>-1:"|="===n&&(o===i||o.slice(0,i.length+1)===i+"-"))}},CHILD:function(e,t,n,i,r){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===i&&0===r?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,d,p,h,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,v=a&&t.nodeName.toLowerCase(),y=!u&&!a,b=!1;if(m){if(o){for(;g;){for(d=t;d=d[g];)if(a?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[s?m.firstChild:m.lastChild],s&&y){for(d=m,f=d[F]||(d[F]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],p=l[0]===M&&l[1],b=p&&l[2],d=p&&m.childNodes[p];d=++p&&d&&d[g]||(b=p=0)||h.pop();)if(1===d.nodeType&&++b&&d===t){c[e]=[M,p,b];break}}else if(y&&(d=t,f=d[F]||(d[F]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],p=l[0]===M&&l[1],b=p),!1===b)for(;(d=++p&&d&&d[g]||(b=p=0)||h.pop())&&((a?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++b||(y&&(f=d[F]||(d[F]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),c[e]=[M,b]),d!==t)););return(b-=r)===i||b%i==0&&b/i>=0}}},PSEUDO:function(e,n){var r,o=w.pseudos[e]||w.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[F]?o(n):o.length>1?(r=[e,e,"",n],w.setFilters.hasOwnProperty(e.toLowerCase())?i(function(e,t){for(var i,r=o(e,n),s=r.length;s--;)i=K(e,r[s]),e[i]=!(t[i]=r[s])}):function(e){return o(e,0,r)}):o}},pseudos:{not:i(function(e){var t=[],n=[],r=E(e.replace(oe,"$1"));return r[F]?i(function(e,t,n,i){for(var o,s=r(e,null,i,[]),a=e.length;a--;)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:i(function(e){return function(n){return t(e,n).length>0}}),contains:i(function(e){return e=e.replace(ve,ye),function(t){return(t.textContent||t.innerText||T(t)).indexOf(e)>-1}}),lang:i(function(e){return ce.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(ve,ye).toLowerCase(),function(t){var n;do{if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===L},focus:function(e){return e===$.activeElement&&(!$.hasFocus||$.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:a(!1),disabled:a(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!w.pseudos.empty(e)},header:function(e){return pe.test(e.nodeName)},input:function(e){return de.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[n<0?n+t:n]}),even:u(function(e,t){for(var n=0;n=0;)e.push(i);return e}),gt:u(function(e,t,n){for(var i=n<0?n+t:n;++i2&&"ID"===(s=o[0]).type&&9===t.nodeType&&q&&w.relative[o[1].type]){if(!(t=(w.find.ID(s.matches[0].replace(ve,ye),t)||[])[0]))return n;c&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(r=fe.needsContext.test(e)?0:o.length;r--&&(s=o[r],!w.relative[a=s.type]);)if((u=w.find[a])&&(i=u(s.matches[0].replace(ve,ye),me.test(o[0].type)&&l(t.parentNode)||t))){if(o.splice(r,1),!(e=i.length&&f(o)))return Y.apply(n,i),n;break}}return(c||E(e,d))(i,t,!q,n,!t||me.test(e)&&l(t.parentNode)||t),n},x.sortStable=F.split("").sort(z).join("")===F,x.detectDuplicates=!!A,j(),x.sortDetached=r(function(e){return 1&e.compareDocumentPosition($.createElement("fieldset"))}),r(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),x.attributes&&r(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),r(function(e){return null==e.getAttribute("disabled")})||o(Z,function(e,t,n){var i;if(!n)return!0===e[t]?t.toLowerCase():(i=e.getAttributeNode(t))&&i.specified?i.value:null}),t}(n);ye.find=Ce,ye.expr=Ce.selectors,ye.expr[":"]=ye.expr.pseudos,ye.uniqueSort=ye.unique=Ce.uniqueSort,ye.text=Ce.getText,ye.isXMLDoc=Ce.isXML,ye.contains=Ce.contains,ye.escapeSelector=Ce.escape;var ke=function(e,t,n){for(var i=[],r=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(r&&ye(e).is(n))break;i.push(e)}return i},Ee=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},Se=ye.expr.match.needsContext,Ne=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,De=/^.[^:#\[\.,]*$/;ye.filter=function(e,t,n){var i=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===i.nodeType?ye.find.matchesSelector(i,e)?[i]:[]:ye.find.matches(e,ye.grep(t,function(e){return 1===e.nodeType}))},ye.fn.extend({find:function(e){var t,n,i=this.length,r=this;if("string"!=typeof e)return this.pushStack(ye(e).filter(function(){for(t=0;t1?ye.uniqueSort(n):n},filter:function(e){return this.pushStack(l(this,e||[],!1))},not:function(e){return this.pushStack(l(this,e||[],!0))},is:function(e){return!!l(this,"string"==typeof e&&Se.test(e)?ye(e):e||[],!1).length}});var Ae,je=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(ye.fn.init=function(e,t,n){var i,r;if(!e)return this;if(n=n||Ae,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:je.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof ye?t[0]:t,ye.merge(this,ye.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:se,!0)),Ne.test(i[1])&&ye.isPlainObject(t))for(i in t)ye.isFunction(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return r=se.getElementById(i[2]),r&&(this[0]=r,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):ye.isFunction(e)?void 0!==n.ready?n.ready(e):e(ye):ye.makeArray(e,this)}).prototype=ye.fn,Ae=ye(se);var $e=/^(?:parents|prev(?:Until|All))/,Le={children:!0,contents:!0,next:!0,prev:!0};ye.fn.extend({has:function(e){var t=ye(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&ye.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?ye.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?fe.call(ye(e),this[0]):fe.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(ye.uniqueSort(ye.merge(this.get(),ye(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),ye.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return ke(e,"parentNode")},parentsUntil:function(e,t,n){return ke(e,"parentNode",n)},next:function(e){return c(e,"nextSibling")},prev:function(e){return c(e,"previousSibling")},nextAll:function(e){return ke(e,"nextSibling")},prevAll:function(e){return ke(e,"previousSibling")},nextUntil:function(e,t,n){return ke(e,"nextSibling",n)},prevUntil:function(e,t,n){return ke(e,"previousSibling",n)},siblings:function(e){return Ee((e.parentNode||{}).firstChild,e)},children:function(e){return Ee(e.firstChild)},contents:function(e){return u(e,"iframe")?e.contentDocument:(u(e,"template")&&(e=e.content||e),ye.merge([],e.childNodes))}},function(e,t){ye.fn[e]=function(n,i){var r=ye.map(this,t,n);return"Until"!==e.slice(-5)&&(i=n),i&&"string"==typeof i&&(r=ye.filter(i,r)),this.length>1&&(Le[e]||ye.uniqueSort(r),$e.test(e)&&r.reverse()),this.pushStack(r)}});var qe=/[^\x20\t\r\n\f]+/g;ye.Callbacks=function(e){e="string"==typeof e?f(e):ye.extend({},e);var t,n,i,r,o=[],s=[],a=-1,u=function(){for(r=r||e.once,i=t=!0;s.length;a=-1)for(n=s.shift();++a-1;)o.splice(n,1),n<=a&&a--}),this},has:function(e){return e?ye.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return r=s=[],o=n="",this},disabled:function(){return!o},lock:function(){return r=s=[],n||t||(o=n=""),this},locked:function(){return!!r},fireWith:function(e,n){return r||(n=n||[],n=[e,n.slice?n.slice():n],s.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!i}};return l},ye.extend({Deferred:function(e){var t=[["notify","progress",ye.Callbacks("memory"),ye.Callbacks("memory"),2],["resolve","done",ye.Callbacks("once memory"),ye.Callbacks("once memory"),0,"resolved"],["reject","fail",ye.Callbacks("once memory"),ye.Callbacks("once memory"),1,"rejected"]],i="pending",r={state:function(){return i},always:function(){return o.done(arguments).fail(arguments),this},catch:function(e){return r.then(null,e)},pipe:function(){var e=arguments;return ye.Deferred(function(n){ye.each(t,function(t,i){var r=ye.isFunction(e[i[4]])&&e[i[4]];o[i[1]](function(){var e=r&&r.apply(this,arguments);e&&ye.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[i[0]+"With"](this,r?[e]:arguments)})}),e=null}).promise()},then:function(e,i,r){function o(e,t,i,r){return function(){var a=this,u=arguments,l=function(){var n,l;if(!(e=s&&(i!==p&&(a=void 0,u=[n]),t.rejectWith(a,u))}};e?c():(ye.Deferred.getStackHook&&(c.stackTrace=ye.Deferred.getStackHook()),n.setTimeout(c))}}var s=0;return ye.Deferred(function(n){t[0][3].add(o(0,n,ye.isFunction(r)?r:d,n.notifyWith)),t[1][3].add(o(0,n,ye.isFunction(e)?e:d)),t[2][3].add(o(0,n,ye.isFunction(i)?i:p))}).promise()},promise:function(e){return null!=e?ye.extend(e,r):r}},o={};return ye.each(t,function(e,n){var s=n[2],a=n[5];r[n[1]]=s.add,a&&s.add(function(){i=a},t[3-e][2].disable,t[0][2].lock),s.add(n[3].fire),o[n[0]]=function(){return o[n[0]+"With"](this===o?void 0:this,arguments),this},o[n[0]+"With"]=s.fireWith}),r.promise(o),e&&e.call(o,o),o},when:function(e){var t=arguments.length,n=t,i=Array(n),r=ue.call(arguments),o=ye.Deferred(),s=function(e){return function(n){i[e]=this,r[e]=arguments.length>1?ue.call(arguments):n,--t||o.resolveWith(i,r)}};if(t<=1&&(h(e,o.done(s(n)).resolve,o.reject,!t),"pending"===o.state()||ye.isFunction(r[n]&&r[n].then)))return o.then();for(;n--;)h(r[n],s(n),o.reject);return o.promise()}});var Oe=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;ye.Deferred.exceptionHook=function(e,t){n.console&&n.console.warn&&e&&Oe.test(e.name)&&n.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},ye.readyException=function(e){n.setTimeout(function(){throw e})};var He=ye.Deferred();ye.fn.ready=function(e){return He.then(e).catch(function(e){ye.readyException(e)}),this},ye.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--ye.readyWait:ye.isReady)||(ye.isReady=!0,!0!==e&&--ye.readyWait>0||He.resolveWith(se,[ye]))}}),ye.ready.then=He.then,"complete"===se.readyState||"loading"!==se.readyState&&!se.documentElement.doScroll?n.setTimeout(ye.ready):(se.addEventListener("DOMContentLoaded",g),n.addEventListener("load",g));var Pe=function(e,t,n,i,r,o,s){var a=0,u=e.length,l=null==n;if("object"===ye.type(n)){r=!0;for(a in n)Pe(e,t,a,n[a],!0,o,s)}else if(void 0!==i&&(r=!0,ye.isFunction(i)||(s=!0),l&&(s?(t.call(e,i),t=null):(l=t,t=function(e,t,n){return l.call(ye(e),n)})),t))for(;a1,null,!0)},removeData:function(e){return this.each(function(){Ie.remove(this,e)})}}),ye.extend({queue:function(e,t,n){var i;if(e)return t=(t||"fx")+"queue",i=Fe.get(e,t),n&&(!i||Array.isArray(n)?i=Fe.access(e,t,ye.makeArray(n)):i.push(n)),i||[]},dequeue:function(e,t){t=t||"fx";var n=ye.queue(e,t),i=n.length,r=n.shift(),o=ye._queueHooks(e,t),s=function(){ye.dequeue(e,t)};"inprogress"===r&&(r=n.shift(),i--),r&&("fx"===t&&n.unshift("inprogress"),delete o.stop,r.call(e,s,o)),!i&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Fe.get(e,n)||Fe.access(e,n,{empty:ye.Callbacks("once memory").add(function(){Fe.remove(e,[t+"queue",n])})})}}),ye.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,Ye=/^$|\/(?:java|ecma)script/i,Qe={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};Qe.optgroup=Qe.option,Qe.tbody=Qe.tfoot=Qe.colgroup=Qe.caption=Qe.thead,Qe.th=Qe.td;var Ke=/<|&#?\w+;/;!function(){var e=se.createDocumentFragment(),t=e.appendChild(se.createElement("div")),n=se.createElement("input");n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),t.appendChild(n),ve.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",ve.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue}();var Ze=se.documentElement,et=/^key/,tt=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,nt=/^([^.]*)(?:\.(.+)|)/;ye.event={global:{},add:function(e,t,n,i,r){var o,s,a,u,l,c,f,d,p,h,g,m=Fe.get(e);if(m)for(n.handler&&(o=n,n=o.handler,r=o.selector),r&&ye.find.matchesSelector(Ze,r),n.guid||(n.guid=ye.guid++),(u=m.events)||(u=m.events={}),(s=m.handle)||(s=m.handle=function(t){return void 0!==ye&&ye.event.triggered!==t.type?ye.event.dispatch.apply(e,arguments):void 0}),t=(t||"").match(qe)||[""],l=t.length;l--;)a=nt.exec(t[l])||[],p=g=a[1],h=(a[2]||"").split(".").sort(),p&&(f=ye.event.special[p]||{},p=(r?f.delegateType:f.bindType)||p,f=ye.event.special[p]||{},c=ye.extend({type:p,origType:g,data:i,handler:n,guid:n.guid,selector:r,needsContext:r&&ye.expr.match.needsContext.test(r),namespace:h.join(".")},o),(d=u[p])||(d=u[p]=[],d.delegateCount=0,f.setup&&!1!==f.setup.call(e,i,h,s)||e.addEventListener&&e.addEventListener(p,s)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),r?d.splice(d.delegateCount++,0,c):d.push(c),ye.event.global[p]=!0)},remove:function(e,t,n,i,r){var o,s,a,u,l,c,f,d,p,h,g,m=Fe.hasData(e)&&Fe.get(e);if(m&&(u=m.events)){for(t=(t||"").match(qe)||[""],l=t.length;l--;)if(a=nt.exec(t[l])||[],p=g=a[1],h=(a[2]||"").split(".").sort(),p){for(f=ye.event.special[p]||{},p=(i?f.delegateType:f.bindType)||p,d=u[p]||[],a=a[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=d.length;o--;)c=d[o],!r&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||i&&i!==c.selector&&("**"!==i||!c.selector)||(d.splice(o,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(e,c));s&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,h,m.handle)||ye.removeEvent(e,p,m.handle),delete u[p])}else for(p in u)ye.event.remove(e,p+t[l],n,i,!0);ye.isEmptyObject(u)&&Fe.remove(e,"handle events")}},dispatch:function(e){var t,n,i,r,o,s,a=ye.event.fix(e),u=new Array(arguments.length),l=(Fe.get(this,"events")||{})[a.type]||[],c=ye.event.special[a.type]||{};for(u[0]=a,t=1;t=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],s={},n=0;n-1:ye.find(r,this,null,[l]).length),s[r]&&o.push(i);o.length&&a.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,rt=/\s*$/g;ye.extend({htmlPrefilter:function(e){return e.replace(it,"<$1>")},clone:function(e,t,n){var i,r,o,s,a=e.cloneNode(!0),u=ye.contains(e.ownerDocument,e);if(!(ve.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||ye.isXMLDoc(e)))for(s=T(a),o=T(e),i=0,r=o.length;i0&&C(s,!u&&T(e,"script")),a},cleanData:function(e){for(var t,n,i,r=ye.event.special,o=0;void 0!==(n=e[o]);o++)if(Re(n)){if(t=n[Fe.expando]){if(t.events)for(i in t.events)r[i]?ye.event.remove(n,i):ye.removeEvent(n,i,t.handle);n[Fe.expando]=void 0}n[Ie.expando]&&(n[Ie.expando]=void 0)}}}),ye.fn.extend({detach:function(e){return H(this,e,!0)},remove:function(e){return H(this,e)},text:function(e){return Pe(this,function(e){return void 0===e?ye.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return O(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){A(this,e).appendChild(e)}})},prepend:function(){return O(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=A(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return O(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return O(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(ye.cleanData(T(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return ye.clone(this,e,t)})},html:function(e){return Pe(this,function(e){var t=this[0]||{},n=0,i=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!rt.test(e)&&!Qe[(Je.exec(e)||["",""])[1].toLowerCase()]){e=ye.htmlPrefilter(e);try{for(;n1)}}),ye.Tween=_,_.prototype={constructor:_,init:function(e,t,n,i,r,o){this.elem=e,this.prop=n,this.easing=r||ye.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=i,this.unit=o||(ye.cssNumber[n]?"":"px")},cur:function(){var e=_.propHooks[this.prop];return e&&e.get?e.get(this):_.propHooks._default.get(this)},run:function(e){var t,n=_.propHooks[this.prop];return this.options.duration?this.pos=t=ye.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):_.propHooks._default.set(this),this}},_.prototype.init.prototype=_.prototype,_.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=ye.css(e.elem,e.prop,""),t&&"auto"!==t?t:0)},set:function(e){ye.fx.step[e.prop]?ye.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[ye.cssProps[e.prop]]&&!ye.cssHooks[e.prop]?e.elem[e.prop]=e.now:ye.style(e.elem,e.prop,e.now+e.unit)}}},_.propHooks.scrollTop=_.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},ye.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},ye.fx=_.prototype.init,ye.fx.step={};var vt,yt,bt=/^(?:toggle|show|hide)$/,xt=/queueHooks$/;ye.Animation=ye.extend(Y,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return b(n.elem,e,_e.exec(t),n),n}]},tweener:function(e,t){ye.isFunction(e)?(t=e,e=["*"]):e=e.match(qe);for(var n,i=0,r=e.length;i1)},removeAttr:function(e){return this.each(function(){ye.removeAttr(this,e)})}}),ye.extend({attr:function(e,t,n){var i,r,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===e.getAttribute?ye.prop(e,t,n):(1===o&&ye.isXMLDoc(e)||(r=ye.attrHooks[t.toLowerCase()]||(ye.expr.match.bool.test(t)?wt:void 0)),void 0!==n?null===n?void ye.removeAttr(e,t):r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=ye.find.attr(e,t),null==i?void 0:i))},attrHooks:{type:{set:function(e,t){if(!ve.radioValue&&"radio"===t&&u(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,i=0,r=t&&t.match(qe);if(r&&1===e.nodeType)for(;n=r[i++];)e.removeAttribute(n)}}),wt={set:function(e,t,n){return!1===t?ye.removeAttr(e,n):e.setAttribute(n,n),n}},ye.each(ye.expr.match.bool.source.match(/\w+/g),function(e,t){var n=Tt[t]||ye.find.attr;Tt[t]=function(e,t,i){var r,o,s=t.toLowerCase();return i||(o=Tt[s],Tt[s]=r,r=null!=n(e,t,i)?s:null,Tt[s]=o),r}});var Ct=/^(?:input|select|textarea|button)$/i,kt=/^(?:a|area)$/i;ye.fn.extend({prop:function(e,t){return Pe(this,ye.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[ye.propFix[e]||e]})}}),ye.extend({prop:function(e,t,n){var i,r,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&ye.isXMLDoc(e)||(t=ye.propFix[t]||t,r=ye.propHooks[t]),void 0!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:e[t]=n:r&&"get"in r&&null!==(i=r.get(e,t))?i:e[t]},propHooks:{tabIndex:{get:function(e){var t=ye.find.attr(e,"tabindex");return t?parseInt(t,10):Ct.test(e.nodeName)||kt.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),ve.optSelected||(ye.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),ye.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){ye.propFix[this.toLowerCase()]=this}),ye.fn.extend({addClass:function(e){var t,n,i,r,o,s,a,u=0;if(ye.isFunction(e))return this.each(function(t){ye(this).addClass(e.call(this,t,K(this)))});if("string"==typeof e&&e)for(t=e.match(qe)||[];n=this[u++];)if(r=K(n),i=1===n.nodeType&&" "+Q(r)+" "){for(s=0;o=t[s++];)i.indexOf(" "+o+" ")<0&&(i+=o+" ");a=Q(i),r!==a&&n.setAttribute("class",a)}return this},removeClass:function(e){var t,n,i,r,o,s,a,u=0;if(ye.isFunction(e))return this.each(function(t){ye(this).removeClass(e.call(this,t,K(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(qe)||[];n=this[u++];)if(r=K(n),i=1===n.nodeType&&" "+Q(r)+" "){for(s=0;o=t[s++];)for(;i.indexOf(" "+o+" ")>-1;)i=i.replace(" "+o+" "," ");a=Q(i),r!==a&&n.setAttribute("class",a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):ye.isFunction(e)?this.each(function(n){ye(this).toggleClass(e.call(this,n,K(this),t),t)}):this.each(function(){var t,i,r,o;if("string"===n)for(i=0,r=ye(this),o=e.match(qe)||[];t=o[i++];)r.hasClass(t)?r.removeClass(t):r.addClass(t);else void 0!==e&&"boolean"!==n||(t=K(this),t&&Fe.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":Fe.get(this,"__className__")||""))})},hasClass:function(e){var t,n,i=0;for(t=" "+e+" ";n=this[i++];)if(1===n.nodeType&&(" "+Q(K(n))+" ").indexOf(t)>-1)return!0;return!1}});var Et=/\r/g;ye.fn.extend({val:function(e){var t,n,i,r=this[0];{if(arguments.length)return i=ye.isFunction(e),this.each(function(n){var r;1===this.nodeType&&(r=i?e.call(this,n,ye(this).val()):e,null==r?r="":"number"==typeof r?r+="":Array.isArray(r)&&(r=ye.map(r,function(e){return null==e?"":e+""})),(t=ye.valHooks[this.type]||ye.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,r,"value")||(this.value=r))});if(r)return(t=ye.valHooks[r.type]||ye.valHooks[r.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(r,"value"))?n:(n=r.value,"string"==typeof n?n.replace(Et,""):null==n?"":n)}}}),ye.extend({valHooks:{option:{get:function(e){var t=ye.find.attr(e,"value");return null!=t?t:Q(ye.text(e))}},select:{get:function(e){var t,n,i,r=e.options,o=e.selectedIndex,s="select-one"===e.type,a=s?null:[],l=s?o+1:r.length;for(i=o<0?l:s?o:0;i-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),ye.each(["radio","checkbox"],function(){ye.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=ye.inArray(ye(e).val(),t)>-1}},ve.checkOn||(ye.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var St=/^(?:focusinfocus|focusoutblur)$/;ye.extend(ye.event,{trigger:function(e,t,i,r){var o,s,a,u,l,c,f,d=[i||se],p=he.call(e,"type")?e.type:e,h=he.call(e,"namespace")?e.namespace.split("."):[];if(s=a=i=i||se,3!==i.nodeType&&8!==i.nodeType&&!St.test(p+ye.event.triggered)&&(p.indexOf(".")>-1&&(h=p.split("."),p=h.shift(),h.sort()),l=p.indexOf(":")<0&&"on"+p,e=e[ye.expando]?e:new ye.Event(p,"object"==typeof e&&e),e.isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=i),t=null==t?[e]:ye.makeArray(t,[e]),f=ye.event.special[p]||{},r||!f.trigger||!1!==f.trigger.apply(i,t))){if(!r&&!f.noBubble&&!ye.isWindow(i)){for(u=f.delegateType||p,St.test(u+p)||(s=s.parentNode);s;s=s.parentNode)d.push(s),a=s;a===(i.ownerDocument||se)&&d.push(a.defaultView||a.parentWindow||n)}for(o=0;(s=d[o++])&&!e.isPropagationStopped();)e.type=o>1?u:f.bindType||p,c=(Fe.get(s,"events")||{})[e.type]&&Fe.get(s,"handle"),c&&c.apply(s,t),(c=l&&s[l])&&c.apply&&Re(s)&&(e.result=c.apply(s,t),!1===e.result&&e.preventDefault());return e.type=p,r||e.isDefaultPrevented()||f._default&&!1!==f._default.apply(d.pop(),t)||!Re(i)||l&&ye.isFunction(i[p])&&!ye.isWindow(i)&&(a=i[l],a&&(i[l]=null),ye.event.triggered=p,i[p](),ye.event.triggered=void 0,a&&(i[l]=a)),e.result}},simulate:function(e,t,n){var i=ye.extend(new ye.Event,n,{type:e,isSimulated:!0});ye.event.trigger(i,null,t)}}),ye.fn.extend({trigger:function(e,t){return this.each(function(){ye.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return ye.event.trigger(e,t,n,!0)}}),ye.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,t){ye.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),ye.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),ve.focusin="onfocusin"in n,ve.focusin||ye.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){ye.event.simulate(t,e.target,ye.event.fix(e))};ye.event.special[t]={setup:function(){var i=this.ownerDocument||this,r=Fe.access(i,t);r||i.addEventListener(e,n,!0),Fe.access(i,t,(r||0)+1)},teardown:function(){var i=this.ownerDocument||this,r=Fe.access(i,t)-1;r?Fe.access(i,t,r):(i.removeEventListener(e,n,!0),Fe.remove(i,t))}}});var Nt=n.location,Dt=ye.now(),At=/\?/;ye.parseXML=function(e){var t;if(!e||"string"!=typeof e)return null;try{t=(new n.DOMParser).parseFromString(e,"text/xml")}catch(e){t=void 0}return t&&!t.getElementsByTagName("parsererror").length||ye.error("Invalid XML: "+e),t};var jt=/\[\]$/,$t=/\r?\n/g,Lt=/^(?:submit|button|image|reset|file)$/i,qt=/^(?:input|select|textarea|keygen)/i;ye.param=function(e,t){var n,i=[],r=function(e,t){var n=ye.isFunction(t)?t():t;i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!ye.isPlainObject(e))ye.each(e,function(){r(this.name,this.value)});else for(n in e)Z(n,e[n],t,r);return i.join("&")},ye.fn.extend({serialize:function(){return ye.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=ye.prop(this,"elements");return e?ye.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!ye(this).is(":disabled")&&qt.test(this.nodeName)&&!Lt.test(e)&&(this.checked||!Ge.test(e))}).map(function(e,t){var n=ye(this).val();return null==n?null:Array.isArray(n)?ye.map(n,function(e){return{name:t.name,value:e.replace($t,"\r\n")}}):{name:t.name,value:n.replace($t,"\r\n")}}).get()}});var Ot=/%20/g,Ht=/#.*$/,Pt=/([?&])_=[^&]*/,Rt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ft=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,It=/^(?:GET|HEAD)$/,Mt=/^\/\//,Wt={},Bt={},_t="*/".concat("*"),Ut=se.createElement("a");Ut.href=Nt.href,ye.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Nt.href,type:"GET",isLocal:Ft.test(Nt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":_t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":ye.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?ne(ne(e,ye.ajaxSettings),t):ne(ye.ajaxSettings,e)},ajaxPrefilter:ee(Wt),ajaxTransport:ee(Bt),ajax:function(e,t){function i(e,t,i,a){var l,d,p,x,w,T=t;c||(c=!0,u&&n.clearTimeout(u),r=void 0,s=a||"",C.readyState=e>0?4:0,l=e>=200&&e<300||304===e,i&&(x=ie(h,C,i)),x=re(h,x,C,l),l?(h.ifModified&&(w=C.getResponseHeader("Last-Modified"),w&&(ye.lastModified[o]=w),(w=C.getResponseHeader("etag"))&&(ye.etag[o]=w)),204===e||"HEAD"===h.type?T="nocontent":304===e?T="notmodified":(T=x.state,d=x.data,p=x.error,l=!p)):(p=T,!e&&T||(T="error",e<0&&(e=0))),C.status=e,C.statusText=(t||T)+"",l?v.resolveWith(g,[d,T,C]):v.rejectWith(g,[C,T,p]),C.statusCode(b),b=void 0,f&&m.trigger(l?"ajaxSuccess":"ajaxError",[C,h,l?d:p]),y.fireWith(g,[C,T]),f&&(m.trigger("ajaxComplete",[C,h]),--ye.active||ye.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,o,s,a,u,l,c,f,d,p,h=ye.ajaxSetup({},t),g=h.context||h,m=h.context&&(g.nodeType||g.jquery)?ye(g):ye.event,v=ye.Deferred(),y=ye.Callbacks("once memory"),b=h.statusCode||{},x={},w={},T="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(c){if(!a)for(a={};t=Rt.exec(s);)a[t[1].toLowerCase()]=t[2];t=a[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?s:null},setRequestHeader:function(e,t){return null==c&&(e=w[e.toLowerCase()]=w[e.toLowerCase()]||e,x[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)C.always(e[C.status]);else for(t in e)b[t]=[b[t],e[t]];return this},abort:function(e){var t=e||T;return r&&r.abort(t),i(0,t),this}};if(v.promise(C),h.url=((e||h.url||Nt.href)+"").replace(Mt,Nt.protocol+"//"),h.type=t.method||t.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(qe)||[""],null==h.crossDomain){l=se.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Ut.protocol+"//"+Ut.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=ye.param(h.data,h.traditional)),te(Wt,h,t,C),c)return C;f=ye.event&&h.global,f&&0==ye.active++&&ye.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!It.test(h.type),o=h.url.replace(Ht,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(Ot,"+")):(p=h.url.slice(o.length),h.data&&(o+=(At.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Pt,"$1"),p=(At.test(o)?"&":"?")+"_="+Dt+++p),h.url=o+p),h.ifModified&&(ye.lastModified[o]&&C.setRequestHeader("If-Modified-Since",ye.lastModified[o]),ye.etag[o]&&C.setRequestHeader("If-None-Match",ye.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||t.contentType)&&C.setRequestHeader("Content-Type",h.contentType),C.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+_t+"; q=0.01":""):h.accepts["*"]);for(d in h.headers)C.setRequestHeader(d,h.headers[d]);if(h.beforeSend&&(!1===h.beforeSend.call(g,C,h)||c))return C.abort();if(T="abort",y.add(h.complete),C.done(h.success),C.fail(h.error),r=te(Bt,h,t,C)){if(C.readyState=1,f&&m.trigger("ajaxSend",[C,h]),c)return C;h.async&&h.timeout>0&&(u=n.setTimeout(function(){C.abort("timeout")},h.timeout));try{c=!1,r.send(x,i)}catch(e){if(c)throw e;i(-1,e)}}else i(-1,"No Transport");return C},getJSON:function(e,t,n){return ye.get(e,t,n,"json")},getScript:function(e,t){return ye.get(e,void 0,t,"script")}}),ye.each(["get","post"],function(e,t){ye[t]=function(e,n,i,r){return ye.isFunction(n)&&(r=r||i,i=n,n=void 0),ye.ajax(ye.extend({url:e,type:t,dataType:r,data:n,success:i},ye.isPlainObject(e)&&e))}}),ye._evalUrl=function(e){return ye.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},ye.fn.extend({wrapAll:function(e){var t;return this[0]&&(ye.isFunction(e)&&(e=e.call(this[0])),t=ye(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return ye.isFunction(e)?this.each(function(t){ye(this).wrapInner(e.call(this,t))}):this.each(function(){var t=ye(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=ye.isFunction(e);return this.each(function(n){ye(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){ye(this).replaceWith(this.childNodes)}),this}}),ye.expr.pseudos.hidden=function(e){return!ye.expr.pseudos.visible(e)},ye.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},ye.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(e){}};var zt={0:200,1223:204},Xt=ye.ajaxSettings.xhr();ve.cors=!!Xt&&"withCredentials"in Xt,ve.ajax=Xt=!!Xt,ye.ajaxTransport(function(e){var t,i;if(ve.cors||Xt&&!e.crossDomain)return{send:function(r,o){var s,a=e.xhr();if(a.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(s in e.xhrFields)a[s]=e.xhrFields[s];e.mimeType&&a.overrideMimeType&&a.overrideMimeType(e.mimeType),e.crossDomain||r["X-Requested-With"]||(r["X-Requested-With"]="XMLHttpRequest");for(s in r)a.setRequestHeader(s,r[s]);t=function(e){return function(){t&&(t=i=a.onload=a.onerror=a.onabort=a.onreadystatechange=null,"abort"===e?a.abort():"error"===e?"number"!=typeof a.status?o(0,"error"):o(a.status,a.statusText):o(zt[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=t(),i=a.onerror=t("error"),void 0!==a.onabort?a.onabort=i:a.onreadystatechange=function(){4===a.readyState&&n.setTimeout(function(){t&&i()})},t=t("abort");try{a.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}}),ye.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),ye.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return ye.globalEval(e),e}}}),ye.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),ye.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,r){t=ye(" - {% endblock %} - - - - .. code-block:: php - - // app/Resources/views/base.html.php - - - - - start('stylesheets') ?> - - stop() ?> - - - - - start('javascripts') ?> - - stop() ?> - - - -That's easy enough! But what if you need to include an extra stylesheet or -JavaScript from a child template? For example, suppose you have a contact -page and you need to include a ``contact.css`` stylesheet *just* on that -page. From inside that contact page's template, do the following: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# app/Resources/views/contact/contact.html.twig #} - {% extends 'base.html.twig' %} - - {% block stylesheets %} - {{ parent() }} - - - {% endblock %} - - {# ... #} - - .. code-block:: php - - // app/Resources/views/contact/contact.html.twig - extend('base.html.php') ?> - - start('stylesheets') ?> - - stop() ?> - -In the child template, you simply override the ``stylesheets`` block and -put your new stylesheet tag inside of that block. Of course, since you want -to add to the parent block's content (and not actually *replace* it), you -should use the ``parent()`` Twig function to include everything from the ``stylesheets`` -block of the base template. - -You can also include assets located in your bundles' ``Resources/public`` folder. -You will need to run the ``php app/console assets:install target [--symlink]`` -command, which moves (or symlinks) files into the correct location. (target -is by default "web"). - -.. code-block:: html+jinja - - - -The end result is a page that includes both the ``main.css`` and ``contact.css`` -stylesheets. - -Global Template Variables -------------------------- - -During each request, Symfony will set a global template variable ``app`` -in both Twig and PHP template engines by default. The ``app`` variable -is a :class:`Symfony\\Bundle\\FrameworkBundle\\Templating\\GlobalVariables` -instance which will give you access to some application specific variables -automatically: - -``app.security`` - The security context. -``app.user`` - The current user object. -``app.request`` - The request object. -``app.session`` - The session object. -``app.environment`` - The current environment (dev, prod, etc). -``app.debug`` - True if in debug mode. False otherwise. - -.. configuration-block:: - - .. code-block:: html+jinja - -

Username: {{ app.user.username }}

- {% if app.debug %} -

Request method: {{ app.request.method }}

-

Application Environment: {{ app.environment }}

- {% endif %} - - .. code-block:: html+php - -

Username: getUser()->getUsername() ?>

- getDebug()): ?> -

Request method: getRequest()->getMethod() ?>

-

Application Environment: getEnvironment() ?>

- - -.. versionadded:: 2.6 - The global ``app.security`` variable (or the ``$app->getSecurity()`` - method in PHP templates) is deprecated as of Symfony 2.6. Use ``app.user`` - (``$app->getUser()``) and ``is_granted()`` (``$view['security']->isGranted()``) - instead. - -.. tip:: - - You can add your own global template variables. See the cookbook example - on :doc:`Global Variables `. - -.. index:: - single: Templating; The templating service - -Configuring and Using the ``templating`` Service ------------------------------------------------- - -The heart of the template system in Symfony is the templating ``Engine``. -This special object is responsible for rendering templates and returning -their content. When you render a template in a controller, for example, -you're actually using the templating engine service. For example:: - - return $this->render('article/index.html.twig'); - -is equivalent to:: - - use Symfony\Component\HttpFoundation\Response; - - $engine = $this->container->get('templating'); - $content = $engine->render('article/index.html.twig'); - - return $response = new Response($content); - -.. _template-configuration: - -The templating engine (or "service") is preconfigured to work automatically -inside Symfony. It can, of course, be configured further in the application -configuration file: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - templating: { engines: ['twig'] } - - .. code-block:: xml - - - - - - - - - twig - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - - 'templating' => array( - 'engines' => array('twig'), - ), - )); - -Several configuration options are available and are covered in the -:doc:`Configuration Appendix `. - -.. note:: - - The ``twig`` engine is mandatory to use the webprofiler (as well as many - third-party bundles). - -.. index:: - single: Template; Overriding templates - -.. _overriding-bundle-templates: - -Overriding Bundle Templates ---------------------------- - -The Symfony community prides itself on creating and maintaining high quality -bundles (see `KnpBundles.com`_) for a large number of different features. -Once you use a third-party bundle, you'll likely need to override and customize -one or more of its templates. - -Suppose you've installed the imaginary open-source AcmeBlogBundle in your -project. And while you're really happy with everything, you want to override -the blog "list" page to customize the markup specifically for your application. -By digging into the ``Blog`` controller of the AcmeBlogBundle, you find the -following:: - - public function indexAction() - { - // some logic to retrieve the blogs - $blogs = ...; - - $this->render( - 'AcmeBlogBundle:Blog:index.html.twig', - array('blogs' => $blogs) - ); - } - -When the ``AcmeBlogBundle:Blog:index.html.twig`` is rendered, Symfony actually -looks in two different locations for the template: - -#. ``app/Resources/AcmeBlogBundle/views/Blog/index.html.twig`` -#. ``src/Acme/BlogBundle/Resources/views/Blog/index.html.twig`` - -To override the bundle template, just copy the ``index.html.twig`` template -from the bundle to ``app/Resources/AcmeBlogBundle/views/Blog/index.html.twig`` -(the ``app/Resources/AcmeBlogBundle`` directory won't exist, so you'll need -to create it). You're now free to customize the template. - -.. caution:: - - If you add a template in a new location, you *may* need to clear your - cache (``php app/console cache:clear``), even if you are in debug mode. - -This logic also applies to base bundle templates. Suppose also that each -template in AcmeBlogBundle inherits from a base template called -``AcmeBlogBundle::layout.html.twig``. Just as before, Symfony will look in -the following two places for the template: - -#. ``app/Resources/AcmeBlogBundle/views/layout.html.twig`` -#. ``src/Acme/BlogBundle/Resources/views/layout.html.twig`` - -Once again, to override the template, just copy it from the bundle to -``app/Resources/AcmeBlogBundle/views/layout.html.twig``. You're now free to -customize this copy as you see fit. - -If you take a step back, you'll see that Symfony always starts by looking in -the ``app/Resources/{BUNDLE_NAME}/views/`` directory for a template. If the -template doesn't exist there, it continues by checking inside the -``Resources/views`` directory of the bundle itself. This means that all bundle -templates can be overridden by placing them in the correct ``app/Resources`` -subdirectory. - -.. note:: - - You can also override templates from within a bundle by using bundle - inheritance. For more information, see :doc:`/cookbook/bundles/inheritance`. - -.. _templating-overriding-core-templates: - -.. index:: - single: Template; Overriding exception templates - -Overriding Core Templates -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Since the Symfony Framework itself is just a bundle, core templates can be -overridden in the same way. For example, the core TwigBundle contains -a number of different "exception" and "error" templates that can be overridden -by copying each from the ``Resources/views/Exception`` directory of the -TwigBundle to, you guessed it, the -``app/Resources/TwigBundle/views/Exception`` directory. - -.. index:: - single: Templating; Three-level inheritance pattern - -Three-level Inheritance ------------------------ - -One common way to use inheritance is to use a three-level approach. This -method works perfectly with the three different types of templates that were just -covered: - -* Create an ``app/Resources/views/base.html.twig`` file that contains the main - layout for your application (like in the previous example). Internally, this - template is called ``base.html.twig``; - -* Create a template for each "section" of your site. For example, the blog - functionality would have a template called ``blog/layout.html.twig`` that - contains only blog section-specific elements; - - .. code-block:: html+jinja - - {# app/Resources/views/blog/layout.html.twig #} - {% extends 'base.html.twig' %} - - {% block body %} -

Blog Application

- - {% block content %}{% endblock %} - {% endblock %} - -* Create individual templates for each page and make each extend the appropriate - section template. For example, the "index" page would be called something - close to ``blog/index.html.twig`` and list the actual blog posts. - - .. code-block:: html+jinja - - {# app/Resources/views/blog/index.html.twig #} - {% extends 'blog/layout.html.twig' %} - - {% block content %} - {% for entry in blog_entries %} -

{{ entry.title }}

-

{{ entry.body }}

- {% endfor %} - {% endblock %} - -Notice that this template extends the section template (``blog/layout.html.twig``) -which in turn extends the base application layout (``base.html.twig``). This is -the common three-level inheritance model. - -When building your application, you may choose to follow this method or simply -make each page template extend the base application template directly -(e.g. ``{% extends 'base.html.twig' %}``). The three-template model is a -best-practice method used by vendor bundles so that the base template for a -bundle can be easily overridden to properly extend your application's base -layout. - -.. index:: - single: Templating; Output escaping - -Output Escaping ---------------- - -When generating HTML from a template, there is always a risk that a template -variable may output unintended HTML or dangerous client-side code. The result -is that dynamic content could break the HTML of the resulting page or allow -a malicious user to perform a `Cross Site Scripting`_ (XSS) attack. Consider -this classic example: - -.. configuration-block:: - - .. code-block:: html+jinja - - Hello {{ name }} - - .. code-block:: html+php - - Hello - -Imagine the user enters the following code for their name: - -.. code-block:: html - - - -Without any output escaping, the resulting template will cause a JavaScript -alert box to pop up: - -.. code-block:: html - - Hello - -And while this seems harmless, if a user can get this far, that same user -should also be able to write JavaScript that performs malicious actions -inside the secure area of an unknowing, legitimate user. - -The answer to the problem is output escaping. With output escaping on, the -same template will render harmlessly, and literally print the ``script`` -tag to the screen: - -.. code-block:: html - - Hello <script>alert('hello!')</script> - -The Twig and PHP templating systems approach the problem in different ways. -If you're using Twig, output escaping is on by default and you're protected. -In PHP, output escaping is not automatic, meaning you'll need to manually -escape where necessary. - -Output Escaping in Twig -~~~~~~~~~~~~~~~~~~~~~~~ - -If you're using Twig templates, then output escaping is on by default. This -means that you're protected out-of-the-box from the unintentional consequences -of user-submitted code. By default, the output escaping assumes that content -is being escaped for HTML output. - -In some cases, you'll need to disable output escaping when you're rendering -a variable that is trusted and contains markup that should not be escaped. -Suppose that administrative users are able to write articles that contain -HTML code. By default, Twig will escape the article body. - -To render it normally, add the ``raw`` filter: - -.. code-block:: jinja - - {{ article.body|raw }} - -You can also disable output escaping inside a ``{% block %}`` area or -for an entire template. For more information, see `Output Escaping`_ in -the Twig documentation. - -Output Escaping in PHP -~~~~~~~~~~~~~~~~~~~~~~ - -Output escaping is not automatic when using PHP templates. This means that -unless you explicitly choose to escape a variable, you're not protected. To -use output escaping, use the special ``escape()`` view method: - -.. code-block:: html+php - - Hello escape($name) ?> - -By default, the ``escape()`` method assumes that the variable is being rendered -within an HTML context (and thus the variable is escaped to be safe for HTML). -The second argument lets you change the context. For example, to output something -in a JavaScript string, use the ``js`` context: - -.. code-block:: html+php - - var myMsg = 'Hello escape($name, 'js') ?>'; - -.. index:: - single: Templating; Formats - -Debugging ---------- - -When using PHP, you can use the -:ref:`dump() function from the VarDumper component ` -if you need to quickly find the value of a variable passed. This is useful, -for example, inside your controller:: - - // src/AppBundle/Controller/ArticleController.php - namespace AppBundle\Controller; - - // ... - - class ArticleController extends Controller - { - public function recentListAction() - { - $articles = ...; - dump($articles); - - // ... - } - } - -.. note:: - - The output of the ``dump()`` function is then rendered in the web developer - toolbar. - -The same mechanism can be used in Twig templates thanks to ``dump`` function: - -.. code-block:: html+jinja - - {# app/Resources/views/article/recent_list.html.twig #} - {{ dump(articles) }} - - {% for article in articles %} - - {{ article.title }} - - {% endfor %} - -The variables will only be dumped if Twig's ``debug`` setting (in ``config.yml``) -is ``true``. By default this means that the variables will be dumped in the -``dev`` environment but not the ``prod`` environment. - -Syntax Checking ---------------- - -You can check for syntax errors in Twig templates using the ``twig:lint`` -console command: - -.. code-block:: bash - - # You can check by filename: - $ php app/console twig:lint app/Resources/views/article/recent_list.html.twig - - # or by directory: - $ php app/console twig:lint app/Resources/views - -.. _template-formats: - -Template Formats ----------------- - -Templates are a generic way to render content in *any* format. And while in -most cases you'll use templates to render HTML content, a template can just -as easily generate JavaScript, CSS, XML or any other format you can dream of. - -For example, the same "resource" is often rendered in several formats. -To render an article index page in XML, simply include the format in the -template name: - -* *XML template name*: ``article/index.xml.twig`` -* *XML template filename*: ``index.xml.twig`` - -In reality, this is nothing more than a naming convention and the template -isn't actually rendered differently based on its format. - -In many cases, you may want to allow a single controller to render multiple -different formats based on the "request format". For that reason, a common -pattern is to do the following:: - - public function indexAction(Request $request) - { - $format = $request->getRequestFormat(); - - return $this->render('article/index.'.$format.'.twig'); - } - -The ``getRequestFormat`` on the ``Request`` object defaults to ``html``, -but can return any other format based on the format requested by the user. -The request format is most often managed by the routing, where a route can -be configured so that ``/contact`` sets the request format to ``html`` while -``/contact.xml`` sets the format to ``xml``. For more information, see the -:ref:`Advanced Example in the Routing chapter `. - -To create links that include the format parameter, include a ``_format`` -key in the parameter hash: - -.. configuration-block:: - - .. code-block:: html+jinja - - - PDF Version - - - .. code-block:: html+php - - - PDF Version - - -Final Thoughts --------------- - -The templating engine in Symfony is a powerful tool that can be used each time -you need to generate presentational content in HTML, XML or any other format. -And though templates are a common way to generate content in a controller, -their use is not mandatory. The ``Response`` object returned by a controller -can be created with or without the use of a template:: - - // creates a Response object whose content is the rendered template - $response = $this->render('article/index.html.twig'); - - // creates a Response object whose content is simple text - $response = new Response('response content'); - -Symfony's templating engine is very flexible and two different template -renderers are available by default: the traditional *PHP* templates and the -sleek and powerful *Twig* templates. Both support a template hierarchy and -come packaged with a rich set of helper functions capable of performing -the most common tasks. - -Overall, the topic of templating should be thought of as a powerful tool -that's at your disposal. In some cases, you may not need to render a template, -and in Symfony, that's absolutely fine. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/templating/PHP` -* :doc:`/cookbook/controller/error_pages` -* :doc:`/cookbook/templating/twig_extension` - -.. _`Twig`: http://twig.sensiolabs.org -.. _`KnpBundles.com`: http://knpbundles.com -.. _`Cross Site Scripting`: http://en.wikipedia.org/wiki/Cross-site_scripting -.. _`Output Escaping`: http://twig.sensiolabs.org/doc/api.html#escaper-extension -.. _`tags`: http://twig.sensiolabs.org/doc/tags/index.html -.. _`filters`: http://twig.sensiolabs.org/doc/filters/index.html -.. _`add your own extensions`: http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension -.. _`hinclude.js`: http://mnot.github.com/hinclude/ -.. _`with_context`: http://twig.sensiolabs.org/doc/functions/include.html -.. _`include() function`: http://twig.sensiolabs.org/doc/functions/include.html -.. _`{% include %} tag`: http://twig.sensiolabs.org/doc/tags/include.html diff --git a/book/translation.rst b/book/translation.rst deleted file mode 100644 index 0b974f15a30..00000000000 --- a/book/translation.rst +++ /dev/null @@ -1,892 +0,0 @@ -.. index:: - single: Translations - -Translations -============ - -The term "internationalization" (often abbreviated `i18n`_) refers to the -process of abstracting strings and other locale-specific pieces out of your -application into a layer where they can be translated and converted based -on the user's locale (i.e. language and country). For text, this means -wrapping each with a function capable of translating the text (or "message") -into the language of the user:: - - // text will *always* print out in English - dump('Hello World'); - die(); - - // text can be translated into the end-user's language or - // default to English - dump($translator->trans('Hello World')); - die(); - -.. note:: - - The term *locale* refers roughly to the user's language and country. It - can be any string that your application uses to manage translations and - other format differences (e.g. currency format). The `ISO 639-1`_ - *language* code, an underscore (``_``), then the `ISO 3166-1 alpha-2`_ - *country* code (e.g. ``fr_FR`` for French/France) is recommended. - -In this chapter, you'll learn how to use the Translation component in the -Symfony Framework. You can read the -:doc:`Translation component documentation ` -to learn even more. Overall, the process has several steps: - -#. :ref:`Enable and configure ` Symfony's - translation service; - -#. Abstract strings (i.e. "messages") by wrapping them in calls to the - ``Translator`` (":ref:`book-translation-basic`"); - -#. :ref:`Create translation resources/files ` - for each supported locale that translate each message in the application; - -#. Determine, :ref:`set and manage the user's locale ` - for the request and optionally - :doc:`on the user's entire session `. - -.. _book-translation-configuration: - -Configuration -------------- - -Translations are handled by a ``translator`` :term:`service` that uses the -user's locale to lookup and return translated messages. Before using it, -enable the ``translator`` in your configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - translator: { fallbacks: [en] } - - .. code-block:: xml - - - - - - - - en - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'translator' => array('fallbacks' => array('en')), - )); - -See :ref:`book-translation-fallback` for details on the ``fallbacks`` key -and what Symfony does when it doesn't find a translation. - -The locale used in translations is the one stored on the request. This is -typically set via a ``_locale`` attribute on your routes (see :ref:`book-translation-locale-url`). - -.. _book-translation-basic: - -Basic Translation ------------------ - -Translation of text is done through the ``translator`` service -(:class:`Symfony\\Component\\Translation\\Translator`). To translate a block -of text (called a *message*), use the -:method:`Symfony\\Component\\Translation\\Translator::trans` method. Suppose, -for example, that you're translating a simple message from inside a controller:: - - // ... - use Symfony\Component\HttpFoundation\Response; - - public function indexAction() - { - $translated = $this->get('translator')->trans('Symfony is great'); - - return new Response($translated); - } - -.. _book-translation-resources: - -When this code is executed, Symfony will attempt to translate the message -"Symfony is great" based on the ``locale`` of the user. For this to work, -you need to tell Symfony how to translate the message via a "translation -resource", which is usually a file that contains a collection of translations -for a given locale. This "dictionary" of translations can be created in several -different formats, XLIFF being the recommended format: - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - Symfony is great - J'aime Symfony - - - - - - .. code-block:: yaml - - # messages.fr.yml - Symfony is great: J'aime Symfony - - .. code-block:: php - - // messages.fr.php - return array( - 'Symfony is great' => 'J\'aime Symfony', - ); - -For information on where these files should be located, see -:ref:`book-translation-resource-locations`. - -Now, if the language of the user's locale is French (e.g. ``fr_FR`` or ``fr_BE``), -the message will be translated into ``J'aime Symfony``. You can also translate -the message inside your :ref:`templates `. - -The Translation Process -~~~~~~~~~~~~~~~~~~~~~~~ - -To actually translate the message, Symfony uses a simple process: - -* The ``locale`` of the current user, which is stored on the request is determined; - -* A catalog (e.g. big collection) of translated messages is loaded from translation - resources defined for the ``locale`` (e.g. ``fr_FR``). Messages from the - :ref:`fallback locale ` are also loaded and - added to the catalog if they don't already exist. The end result is a large - "dictionary" of translations. - -* If the message is located in the catalog, the translation is returned. If - not, the translator returns the original message. - -When using the ``trans()`` method, Symfony looks for the exact string inside -the appropriate message catalog and returns it (if it exists). - -Message Placeholders --------------------- - -Sometimes, a message containing a variable needs to be translated:: - - use Symfony\Component\HttpFoundation\Response; - - public function indexAction($name) - { - $translated = $this->get('translator')->trans('Hello '.$name); - - return new Response($translated); - } - -However, creating a translation for this string is impossible since the translator -will try to look up the exact message, including the variable portions -(e.g. *"Hello Ryan"* or *"Hello Fabien"*). - -For details on how to handle this situation, see :ref:`component-translation-placeholders` -in the components documentation. For how to do this in templates, see :ref:`book-translation-tags`. - -Pluralization -------------- - -Another complication is when you have translations that may or may not be -plural, based on some variable: - -.. code-block:: text - - There is one apple. - There are 5 apples. - -To handle this, use the :method:`Symfony\\Component\\Translation\\Translator::transChoice` -method or the ``transchoice`` tag/filter in your :ref:`template `. - -For much more information, see :ref:`component-translation-pluralization` -in the Translation component documentation. - -Translations in Templates -------------------------- - -Most of the time, translation occurs in templates. Symfony provides native -support for both Twig and PHP templates. - -.. _book-translation-tags: - -Twig Templates -~~~~~~~~~~~~~~ - -Symfony provides specialized Twig tags (``trans`` and ``transchoice``) to -help with message translation of *static blocks of text*: - -.. code-block:: jinja - - {% trans %}Hello %name%{% endtrans %} - - {% transchoice count %} - {0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples - {% endtranschoice %} - -The ``transchoice`` tag automatically gets the ``%count%`` variable from -the current context and passes it to the translator. This mechanism only -works when you use a placeholder following the ``%var%`` pattern. - -.. caution:: - - The ``%var%`` notation of placeholders is required when translating in - Twig templates using the tag. - -.. tip:: - - If you need to use the percent character (``%``) in a string, escape it by - doubling it: ``{% trans %}Percent: %percent%%%{% endtrans %}`` - -You can also specify the message domain and pass some additional variables: - -.. code-block:: jinja - - {% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %} - - {% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %} - - {% transchoice count with {'%name%': 'Fabien'} from "app" %} - {0} %name%, there are no apples|{1} %name%, there is one apple|]1,Inf[ %name%, there are %count% apples - {% endtranschoice %} - -.. _book-translation-filters: - -The ``trans`` and ``transchoice`` filters can be used to translate *variable -texts* and complex expressions: - -.. code-block:: jinja - - {{ message|trans }} - - {{ message|transchoice(5) }} - - {{ message|trans({'%name%': 'Fabien'}, "app") }} - - {{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }} - -.. tip:: - - Using the translation tags or filters have the same effect, but with - one subtle difference: automatic output escaping is only applied to - translations using a filter. In other words, if you need to be sure - that your translated message is *not* output escaped, you must apply - the ``raw`` filter after the translation filter: - - .. code-block:: jinja - - {# text translated between tags is never escaped #} - {% trans %} -

foo

- {% endtrans %} - - {% set message = '

foo

' %} - - {# strings and variables translated via a filter are escaped by default #} - {{ message|trans|raw }} - {{ '

bar

'|trans|raw }} - -.. tip:: - - You can set the translation domain for an entire Twig template with a single tag: - - .. code-block:: jinja - - {% trans_default_domain "app" %} - - Note that this only influences the current template, not any "included" - template (in order to avoid side effects). - -PHP Templates -~~~~~~~~~~~~~ - -The translator service is accessible in PHP templates through the -``translator`` helper: - -.. code-block:: html+php - - trans('Symfony is great') ?> - - transChoice( - '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples', - 10, - array('%count%' => 10) - ) ?> - -.. _book-translation-resource-locations: - -Translation Resource/File Names and Locations ---------------------------------------------- - -Symfony looks for message files (i.e. translations) in the following locations: - -* the ``app/Resources/translations`` directory; - -* the ``app/Resources//translations`` directory; - -* the ``Resources/translations/`` directory inside of any bundle. - -The locations are listed here with the highest priority first. That is, you can -override the translation messages of a bundle in any of the top 2 directories. - -The override mechanism works at a key level: only the overridden keys need -to be listed in a higher priority message file. When a key is not found -in a message file, the translator will automatically fall back to the lower -priority message files. - -The filename of the translation files is also important: each message file -must be named according to the following path: ``domain.locale.loader``: - -* **domain**: An optional way to organize messages into groups (e.g. ``admin``, - ``navigation`` or the default ``messages``) - see :ref:`using-message-domains`; - -* **locale**: The locale that the translations are for (e.g. ``en_GB``, ``en``, etc); - -* **loader**: How Symfony should load and parse the file (e.g. ``xlf``, - ``php``, ``yml``, etc). - -The loader can be the name of any registered loader. By default, Symfony -provides many loaders, including: - -* ``xlf``: XLIFF file; -* ``php``: PHP file; -* ``yml``: YAML file. - -The choice of which loader to use is entirely up to you and is a matter of -taste. The recommended option is to use ``xlf`` for translations. -For more options, see :ref:`component-translator-message-catalogs`. - -.. note:: - - You can also store translations in a database, or any other storage by - providing a custom class implementing the - :class:`Symfony\\Component\\Translation\\Loader\\LoaderInterface` interface. - See the :ref:`dic-tags-translation-loader` tag for more information. - -.. caution:: - - Each time you create a *new* translation resource (or install a bundle - that includes a translation resource), be sure to clear your cache so - that Symfony can discover the new translation resources: - - .. code-block:: bash - - $ php app/console cache:clear - -.. _book-translation-fallback: - -Fallback Translation Locales ----------------------------- - -Imagine that the user's locale is ``fr_FR`` and that you're translating the -key ``Symfony is great``. To find the French translation, Symfony actually -checks translation resources for several locales: - -#. First, Symfony looks for the translation in a ``fr_FR`` translation resource - (e.g. ``messages.fr_FR.xlf``); - -#. If it wasn't found, Symfony looks for the translation in a ``fr`` translation - resource (e.g. ``messages.fr.xlf``); - -#. If the translation still isn't found, Symfony uses the ``fallbacks`` configuration - parameter, which defaults to ``en`` (see `Configuration`_). - -.. versionadded:: 2.6 - The ability to log missing translations was introduced in Symfony 2.6. - -.. note:: - - When Symfony doesn't find a translation in the given locale, it will - add the missing translation to the log file. For details, - see :ref:`reference-framework-translator-logging`. - -.. _book-translation-user-locale: - -Handling the User's Locale --------------------------- - -The locale of the current user is stored in the request and is accessible -via the ``request`` object:: - - use Symfony\Component\HttpFoundation\Request; - - public function indexAction(Request $request) - { - $locale = $request->getLocale(); - } - -To set the user's locale, you may want to create a custom event listener -so that it's set before any other parts of the system (i.e. the translator) -need it:: - - public function onKernelRequest(GetResponseEvent $event) - { - $request = $event->getRequest(); - - // some logic to determine the $locale - $request->getSession()->set('_locale', $locale); - } - -Read :doc:`/cookbook/session/locale_sticky_session` for more on the topic. - -.. note:: - - Setting the locale using ``$request->setLocale()`` in the controller - is too late to affect the translator. Either set the locale via a listener - (like above), the URL (see next) or call ``setLocale()`` directly on - the ``translator`` service. - -See the :ref:`book-translation-locale-url` section below about setting the -locale via routing. - -.. _book-translation-locale-url: - -The Locale and the URL -~~~~~~~~~~~~~~~~~~~~~~ - -Since you can store the locale of the user in the session, it may be tempting -to use the same URL to display a resource in different languages based -on the user's locale. For example, ``http://www.example.com/contact`` could -show content in English for one user and French for another user. Unfortunately, -this violates a fundamental rule of the Web: that a particular URL returns -the same resource regardless of the user. To further muddy the problem, which -version of the content would be indexed by search engines? - -A better policy is to include the locale in the URL. This is fully-supported -by the routing system using the special ``_locale`` parameter: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/routing.yml - contact: - path: /{_locale}/contact - defaults: { _controller: AppBundle:Contact:index } - requirements: - _locale: en|fr|de - - .. code-block:: xml - - - - - - - AppBundle:Contact:index - en|fr|de - - - - .. code-block:: php - - // app/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('contact', new Route( - '/{_locale}/contact', - array( - '_controller' => 'AppBundle:Contact:index', - ), - array( - '_locale' => 'en|fr|de', - ) - )); - - return $collection; - -When using the special ``_locale`` parameter in a route, the matched locale -will *automatically be set on the Request* and can be retrieved via the -:method:`Symfony\\Component\\HttpFoundation\\Request::getLocale` method. -In other words, if a user -visits the URI ``/fr/contact``, the locale ``fr`` will automatically be set -as the locale for the current request. - -You can now use the locale to create routes to other translated pages -in your application. - -.. tip:: - - Read :doc:`/cookbook/routing/service_container_parameters` to learn how to - avoid hardcoding the ``_locale`` requirement in all your routes. - -.. index:: - single: Translations; Fallback and default locale - -.. _book-translation-default-locale: - -Setting a Default Locale -~~~~~~~~~~~~~~~~~~~~~~~~ - -What if the user's locale hasn't been determined? You can guarantee that a -locale is set on each user's request by defining a ``default_locale`` for -the framework: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - default_locale: en - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'default_locale' => 'en', - )); - -.. _book-translation-constraint-messages: - -Translating Constraint Messages -------------------------------- - -If you're using validation constraints with the Form component, then translating -the error messages is easy: simply create a translation resource for the -``validators`` :ref:`domain `. - -To start, suppose you've created a plain-old-PHP object that you need to -use somewhere in your application:: - - // src/AppBundle/Entity/Author.php - namespace AppBundle\Entity; - - class Author - { - public $name; - } - -Add constraints though any of the supported methods. Set the message option to the -translation source text. For example, to guarantee that the ``$name`` property is -not empty, add the following: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/Author.php - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank(message = "author.name.not_blank") - */ - public $name; - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\Author: - properties: - name: - - NotBlank: { message: "author.name.not_blank" } - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - - class Author - { - public $name; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('name', new NotBlank(array( - 'message' => 'author.name.not_blank', - ))); - } - } - -Create a translation file under the ``validators`` catalog for the constraint -messages, typically in the ``Resources/translations/`` directory of the -bundle. - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - author.name.not_blank - Please enter an author name. - - - - - - .. code-block:: yaml - - # validators.en.yml - author.name.not_blank: Please enter an author name. - - .. code-block:: php - - // validators.en.php - return array( - 'author.name.not_blank' => 'Please enter an author name.', - ); - -Translating Database Content ----------------------------- - -The translation of database content should be handled by Doctrine through -the `Translatable Extension`_ or the `Translatable Behavior`_ (PHP 5.4+). -For more information, see the documentation for these libraries. - -Debugging Translations ----------------------- - -.. versionadded:: 2.6 - Prior to Symfony 2.6, this command was called ``translation:debug``. - -When maintaining a bundle, you may use or remove the usage of a translation -message without updating all message catalogues. The ``debug:translation`` -command helps you to find these missing or unused translation messages for a -given locale. It shows you a table with the result when translating the -message in the given locale and the result when the fallback would be used. -On top of that, it also shows you when the translation is the same as the -fallback translation (this could indicate that the message was not correctly -translated). - -Thanks to the messages extractors, the command will detect the translation -tag or filter usages in Twig templates: - -.. code-block:: jinja - - {% trans %}Symfony2 is great{% endtrans %} - - {{ 'Symfony2 is great'|trans }} - - {{ 'Symfony2 is great'|transchoice(1) }} - - {% transchoice 1 %}Symfony2 is great{% endtranschoice %} - -It will also detect the following translator usages in PHP templates: - -.. code-block:: php - - $view['translator']->trans("Symfony2 is great"); - - $view['translator']->transChoice('Symfony2 is great', 1); - -.. caution:: - - The extractors are not able to inspect the messages translated outside templates which means - that translator usages in form labels or inside your controllers won't be detected. - Dynamic translations involving variables or expressions are not detected in templates, - which means this example won't be analyzed: - - .. code-block:: jinja - - {% set message = 'Symfony2 is great' %} - {{ message|trans }} - -Suppose your application's default_locale is ``fr`` and you have configured ``en`` as the fallback locale -(see :ref:`book-translation-configuration` and :ref:`book-translation-fallback` for how to configure these). -And suppose you've already setup some translations for the ``fr`` locale inside an AcmeDemoBundle: - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - Symfony2 is great - J'aime Symfony2 - - - - - - - .. code-block:: yaml - - # src/Acme/AcmeDemoBundle/Resources/translations/messages.fr.yml - Symfony2 is great: J'aime Symfony2 - - .. code-block:: php - - // src/Acme/AcmeDemoBundle/Resources/translations/messages.fr.php - return array( - 'Symfony2 is great' => 'J\'aime Symfony2', - ); - -and for the ``en`` locale: - -.. configuration-block:: - - .. code-block:: xml - - - - - - - - Symfony2 is great - Symfony2 is great - - - - - - .. code-block:: yaml - - # src/Acme/AcmeDemoBundle/Resources/translations/messages.en.yml - Symfony2 is great: Symfony2 is great - - .. code-block:: php - - // src/Acme/AcmeDemoBundle/Resources/translations/messages.en.php - return array( - 'Symfony2 is great' => 'Symfony2 is great', - ); - -To inspect all messages in the ``fr`` locale for the AcmeDemoBundle, run: - -.. code-block:: bash - - $ php app/console debug:translation fr AcmeDemoBundle - -You will get this output: - -.. image:: /images/book/translation/debug_1.png - :align: center - -It indicates that the message ``Symfony2 is great`` is unused because it is translated, -but you haven't used it anywhere yet. - -Now, if you translate the message in one of your templates, you will get this output: - -.. image:: /images/book/translation/debug_2.png - :align: center - -The state is empty which means the message is translated in the ``fr`` locale and used in one or more templates. - -If you delete the message ``Symfony2 is great`` from your translation file for the ``fr`` locale -and run the command, you will get: - -.. image:: /images/book/translation/debug_3.png - :align: center - -The state indicates the message is missing because it is not translated in the ``fr`` locale -but it is still used in the template. -Moreover, the message in the ``fr`` locale equals to the message in the ``en`` locale. -This is a special case because the untranslated message id equals its translation in the ``en`` locale. - -If you copy the content of the translation file in the ``en`` locale, to the translation file -in the ``fr`` locale and run the command, you will get: - -.. image:: /images/book/translation/debug_4.png - :align: center - -You can see that the translations of the message are identical in the ``fr`` and ``en`` locales -which means this message was probably copied from French to English and maybe you forgot to translate it. - -By default all domains are inspected, but it is possible to specify a single domain: - -.. code-block:: bash - - $ php app/console debug:translation en AcmeDemoBundle --domain=messages - -When bundles have a lot of messages, it is useful to display only the unused -or only the missing messages, by using the ``--only-unused`` or ``--only-missing`` switches: - -.. code-block:: bash - - $ php app/console debug:translation en AcmeDemoBundle --only-unused - $ php app/console debug:translation en AcmeDemoBundle --only-missing - -Summary -------- - -With the Symfony Translation component, creating an internationalized application -no longer needs to be a painful process and boils down to just a few basic -steps: - -* Abstract messages in your application by wrapping each in either the - :method:`Symfony\\Component\\Translation\\Translator::trans` or - :method:`Symfony\\Component\\Translation\\Translator::transChoice` methods - (learn about this in :doc:`/components/translation/usage`); - -* Translate each message into multiple locales by creating translation message - files. Symfony discovers and processes each file because its name follows - a specific convention; - -* Manage the user's locale, which is stored on the request, but can also - be set on the user's session. - -.. _`i18n`: http://en.wikipedia.org/wiki/Internationalization_and_localization -.. _`ISO 3166-1 alpha-2`: http://en.wikipedia.org/wiki/ISO_3166-1#Current_codes -.. _`ISO 639-1`: http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes -.. _`Translatable Extension`: https://github.com/l3pp4rd/DoctrineExtensions -.. _`Translatable Behavior`: https://github.com/KnpLabs/DoctrineBehaviors diff --git a/book/validation.rst b/book/validation.rst deleted file mode 100644 index d5f8744b8b5..00000000000 --- a/book/validation.rst +++ /dev/null @@ -1,1304 +0,0 @@ -.. index:: - single: Validation - -Validation -========== - -Validation is a very common task in web applications. Data entered in forms -needs to be validated. Data also needs to be validated before it is written -into a database or passed to a web service. - -Symfony ships with a `Validator`_ component that makes this task easy and -transparent. This component is based on the -`JSR303 Bean Validation specification`_. - -.. index:: - single: Validation; The basics - -The Basics of Validation ------------------------- - -The best way to understand validation is to see it in action. To start, suppose -you've created a plain-old-PHP object that you need to use somewhere in -your application:: - - // src/AppBundle/Entity/Author.php - namespace AppBundle\Entity; - - class Author - { - public $name; - } - -So far, this is just an ordinary class that serves some purpose inside your -application. The goal of validation is to tell you if the data -of an object is valid. For this to work, you'll configure a list of rules -(called :ref:`constraints `) that the object must -follow in order to be valid. These rules can be specified via a number of -different formats (YAML, XML, annotations, or PHP). - -For example, to guarantee that the ``$name`` property is not empty, add the -following: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank() - */ - public $name; - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\Author: - properties: - name: - - NotBlank: ~ - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - - class Author - { - public $name; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('name', new NotBlank()); - } - } - -.. tip:: - - Protected and private properties can also be validated, as well as "getter" - methods (see :ref:`validator-constraint-targets`). - -.. versionadded:: 2.7 - As of Symfony 2.7, XML and Yaml constraint files located in the - ``Resources/config/validation`` sub-directory of a bundle are loaded. Prior - to 2.7, only ``Resources/config/validation.yml`` (or ``.xml``) were loaded. - -.. index:: - single: Validation; Using the validator - -Using the ``validator`` Service -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, to actually validate an ``Author`` object, use the ``validate`` method -on the ``validator`` service (class :class:`Symfony\\Component\\Validator\\Validator`). -The job of the ``validator`` is easy: to read the constraints (i.e. rules) -of a class and verify if the data on the object satisfies those -constraints. If validation fails, a non-empty list of errors -(class :class:`Symfony\\Component\\Validator\\ConstraintViolationList`) is -returned. Take this simple example from inside a controller:: - - // ... - use Symfony\Component\HttpFoundation\Response; - use AppBundle\Entity\Author; - - // ... - public function authorAction() - { - $author = new Author(); - - // ... do something to the $author object - - $validator = $this->get('validator'); - $errors = $validator->validate($author); - - if (count($errors) > 0) { - /* - * Uses a __toString method on the $errors variable which is a - * ConstraintViolationList object. This gives us a nice string - * for debugging. - */ - $errorsString = (string) $errors; - - return new Response($errorsString); - } - - return new Response('The author is valid! Yes!'); - } - -If the ``$name`` property is empty, you will see the following error -message: - -.. code-block:: text - - AppBundle\Author.name: - This value should not be blank - -If you insert a value into the ``name`` property, the happy success message -will appear. - -.. tip:: - - Most of the time, you won't interact directly with the ``validator`` - service or need to worry about printing out the errors. Most of the time, - you'll use validation indirectly when handling submitted form data. For - more information, see the :ref:`book-validation-forms`. - -You could also pass the collection of errors into a template:: - - if (count($errors) > 0) { - return $this->render('author/validation.html.twig', array( - 'errors' => $errors, - )); - } - -Inside the template, you can output the list of errors exactly as needed: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# app/Resources/views/author/validation.html.twig #} -

The author has the following errors

-
    - {% for error in errors %} -
  • {{ error.message }}
  • - {% endfor %} -
- - .. code-block:: html+php - - -

The author has the following errors

-
    - -
  • getMessage() ?>
  • - -
- -.. note:: - - Each validation error (called a "constraint violation"), is represented by - a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object. - -.. index:: - single: Validation; Validation with forms - -.. _book-validation-forms: - -Validation and Forms -~~~~~~~~~~~~~~~~~~~~ - -The ``validator`` service can be used at any time to validate any object. -In reality, however, you'll usually work with the ``validator`` indirectly -when working with forms. Symfony's form library uses the ``validator`` service -internally to validate the underlying object after values have been submitted. -The constraint violations on the object are converted into ``FieldError`` -objects that can easily be displayed with your form. The typical form submission -workflow looks like the following from inside a controller:: - - // ... - use AppBundle\Entity\Author; - use AppBundle\Form\AuthorType; - use Symfony\Component\HttpFoundation\Request; - - // ... - public function updateAction(Request $request) - { - $author = new Author(); - $form = $this->createForm(new AuthorType(), $author); - - $form->handleRequest($request); - - if ($form->isValid()) { - // the validation passed, do something with the $author object - - return $this->redirectToRoute(...); - } - - return $this->render('author/form.html.twig', array( - 'form' => $form->createView(), - )); - } - -.. note:: - - This example uses an ``AuthorType`` form class, which is not shown here. - -For more information, see the :doc:`Forms ` chapter. - -.. index:: - pair: Validation; Configuration - -.. _book-validation-configuration: - -Configuration -------------- - -The Symfony validator is enabled by default, but you must explicitly enable -annotations if you're using the annotation method to specify your constraints: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - validation: { enable_annotations: true } - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'validation' => array( - 'enable_annotations' => true, - ), - )); - -.. index:: - single: Validation; Constraints - -.. _validation-constraints: - -Constraints ------------ - -The ``validator`` is designed to validate objects against *constraints* (i.e. -rules). In order to validate an object, simply map one or more constraints -to its class and then pass it to the ``validator`` service. - -Behind the scenes, a constraint is simply a PHP object that makes an assertive -statement. In real life, a constraint could be: "The cake must not be burned". -In Symfony, constraints are similar: they are assertions that a condition -is true. Given a value, a constraint will tell you if that value -adheres to the rules of the constraint. - -Supported Constraints -~~~~~~~~~~~~~~~~~~~~~ - -Symfony packages many of the most commonly-needed constraints: - -.. include:: /reference/constraints/map.rst.inc - -You can also create your own custom constraints. This topic is covered in -the ":doc:`/cookbook/validation/custom_constraint`" article of the cookbook. - -.. index:: - single: Validation; Constraints configuration - -.. _book-validation-constraint-configuration: - -Constraint Configuration -~~~~~~~~~~~~~~~~~~~~~~~~ - -Some constraints, like :doc:`NotBlank `, -are simple whereas others, like the :doc:`Choice ` -constraint, have several configuration options available. Suppose that the -``Author`` class has another property called ``gender`` that can be set to either -"male", "female" or "other": - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Choice( - * choices = { "male", "female", "other" }, - * message = "Choose a valid gender." - * ) - */ - public $gender; - - // ... - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\Author: - properties: - gender: - - Choice: { choices: [male, female, other], message: Choose a valid gender. } - # ... - - .. code-block:: xml - - - - - - - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - public $gender; - - // ... - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - // ... - - $metadata->addPropertyConstraint('gender', new Assert\Choice(array( - 'choices' => array('male', 'female', 'other'), - 'message' => 'Choose a valid gender.', - ))); - } - } - -.. _validation-default-option: - -The options of a constraint can always be passed in as an array. Some constraints, -however, also allow you to pass the value of one, "*default*", option in place -of the array. In the case of the ``Choice`` constraint, the ``choices`` -options can be specified in this way. - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\Choice({"male", "female", "other"}) - */ - protected $gender; - - // ... - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\Author: - properties: - gender: - - Choice: [male, female, other] - # ... - - .. code-block:: xml - - - - - - - - - male - female - other - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - protected $gender; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - // ... - - $metadata->addPropertyConstraint( - 'gender', - new Assert\Choice(array('male', 'female', 'other')) - ); - } - } - -This is purely meant to make the configuration of the most common option of -a constraint shorter and quicker. - -If you're ever unsure of how to specify an option, either check the API documentation -for the constraint or play it safe by always passing in an array of options -(the first method shown above). - -Translation Constraint Messages -------------------------------- - -For information on translating the constraint messages, see -:ref:`book-translation-constraint-messages`. - -.. index:: - single: Validation; Constraint targets - -.. _validator-constraint-targets: - -Constraint Targets ------------------- - -Constraints can be applied to a class property (e.g. ``name``) or a public -getter method (e.g. ``getFullName``). The first is the most common and easy -to use, but the second allows you to specify more complex validation rules. - -.. index:: - single: Validation; Property constraints - -.. _validation-property-target: - -Properties -~~~~~~~~~~ - -Validating class properties is the most basic validation technique. Symfony -allows you to validate private, protected or public properties. The next -listing shows you how to configure the ``$firstName`` property of an ``Author`` -class to have at least 3 characters. - -.. configuration-block:: - - .. code-block:: php-annotations - - // AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\NotBlank() - * @Assert\Length(min=3) - */ - private $firstName; - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\Author: - properties: - firstName: - - NotBlank: ~ - - Length: - min: 3 - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - private $firstName; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); - $metadata->addPropertyConstraint( - 'firstName', - new Assert\Length(array("min" => 3)) - ); - } - } - -.. index:: - single: Validation; Getter constraints - -Getters -~~~~~~~ - -Constraints can also be applied to the return value of a method. Symfony -allows you to add a constraint to any public method whose name starts with -"get", "is" or "has". In this guide, these types of methods are referred to -as "getters". - -The benefit of this technique is that it allows you to validate your object -dynamically. For example, suppose you want to make sure that a password field -doesn't match the first name of the user (for security reasons). You can -do this by creating an ``isPasswordLegal`` method, and then asserting that -this method must return ``true``: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - /** - * @Assert\True(message = "The password cannot match your first name") - */ - public function isPasswordLegal() - { - // ... return true or false - } - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\Author: - getters: - passwordLegal: - - "True": { message: "The password cannot match your first name" } - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/Author.php - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Author - { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addGetterConstraint('passwordLegal', new Assert\True(array( - 'message' => 'The password cannot match your first name', - ))); - } - } - -Now, create the ``isPasswordLegal()`` method and include the logic you need:: - - public function isPasswordLegal() - { - return $this->firstName !== $this->password; - } - -.. note:: - - The keen-eyed among you will have noticed that the prefix of the getter - ("get", "is" or "has") is omitted in the mapping. This allows you to move - the constraint to a property with the same name later (or vice versa) - without changing your validation logic. - -.. _validation-class-target: - -Classes -~~~~~~~ - -Some constraints apply to the entire class being validated. For example, -the :doc:`Callback ` constraint is a generic -constraint that's applied to the class itself. When that class is validated, -methods specified by that constraint are simply executed so that each can -provide more custom validation. - -.. _book-validation-validation-groups: - -Validation Groups ------------------ - -So far, you've been able to add constraints to a class and ask whether or -not that class passes all the defined constraints. In some cases, however, -you'll need to validate an object against only *some* constraints -on that class. To do this, you can organize each constraint into one or more -"validation groups", and then apply validation against just one group of -constraints. - -For example, suppose you have a ``User`` class, which is used both when a -user registers and when a user updates their contact information later: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Validator\Constraints as Assert; - - class User implements UserInterface - { - /** - * @Assert\Email(groups={"registration"}) - */ - private $email; - - /** - * @Assert\NotBlank(groups={"registration"}) - * @Assert\Length(min=7, groups={"registration"}) - */ - private $password; - - /** - * @Assert\Length(min=2) - */ - private $city; - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\User: - properties: - email: - - Email: { groups: [registration] } - password: - - NotBlank: { groups: [registration] } - - Length: { min: 7, groups: [registration] } - city: - - Length: - min: 2 - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class User - { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('email', new Assert\Email(array( - 'groups' => array('registration'), - ))); - - $metadata->addPropertyConstraint('password', new Assert\NotBlank(array( - 'groups' => array('registration'), - ))); - $metadata->addPropertyConstraint('password', new Assert\Length(array( - 'min' => 7, - 'groups' => array('registration'), - ))); - - $metadata->addPropertyConstraint('city', new Assert\Length(array( - "min" => 3, - ))); - } - } - -With this configuration, there are three validation groups: - -``Default`` - Contains the constraints in the current class and all referenced classes - that belong to no other group. - -``User`` - Equivalent to all constraints of the ``User`` object in the ``Default`` - group. This is always the name of the class. The difference between this - and ``Default`` is explained below. - -``registration`` - Contains the constraints on the ``email`` and ``password`` fields only. - -Constraints in the ``Default`` group of a class are the constraints that have -either no explicit group configured or that are configured to a group equal to -the class name or the string ``Default``. - -.. caution:: - - When validating *just* the User object, there is no difference between the - ``Default`` group and the ``User`` group. But, there is a difference if - ``User`` has embedded objects. For example, imagine ``User`` has an - ``address`` property that contains some ``Address`` object and that you've - added the :doc:`/reference/constraints/Valid` constraint to this property - so that it's validated when you validate the ``User`` object. - - If you validate ``User`` using the ``Default`` group, then any constraints - on the ``Address`` class that are in the ``Default`` group *will* be used. - But, if you validate ``User`` using the ``User`` validation group, then - only constraints on the ``Address`` class with the ``User`` group will be - validated. - - In other words, the ``Default`` group and the class name group (e.g. - ``User``) are identical, except when the class is embedded in another - object that's actually the one being validated. - - If you have inheritance (e.g. ``User extends BaseUser``) and you validate - with the class name of the subclass (i.e. ``User``), then all constraints - in the ``User`` and ``BaseUser`` will be validated. However, if you - validate using the base class (i.e. ``BaseUser``), then only the default - constraints in the ``BaseUser`` class will be validated. - -To tell the validator to use a specific group, pass one or more group names -as the third argument to the ``validate()`` method:: - - // If you're using the new 2.5 validation API (you probably are!) - $errors = $validator->validate($author, null, array('registration')); - - // If you're using the old 2.4 validation API, pass the group names as the second argument - // $errors = $validator->validate($author, array('registration')); - -If no groups are specified, all constraints that belong to the group ``Default`` -will be applied. - -Of course, you'll usually work with validation indirectly through the form -library. For information on how to use validation groups inside forms, see -:ref:`book-forms-validation-groups`. - -.. index:: - single: Validation; Validating raw values - -.. _book-validation-group-sequence: - -Group Sequence --------------- - -In some cases, you want to validate your groups by steps. To do this, you can -use the ``GroupSequence`` feature. In this case, an object defines a group -sequence, which determines the order groups should be validated. - -For example, suppose you have a ``User`` class and want to validate that the -username and the password are different only if all other validation passes -(in order to avoid multiple error messages). - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Validator\Constraints as Assert; - - /** - * @Assert\GroupSequence({"User", "Strict"}) - */ - class User implements UserInterface - { - /** - * @Assert\NotBlank - */ - private $username; - - /** - * @Assert\NotBlank - */ - private $password; - - /** - * @Assert\True(message="The password cannot match your username", groups={"Strict"}) - */ - public function isPasswordLegal() - { - return ($this->username !== $this->password); - } - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\User: - group_sequence: - - User - - Strict - getters: - passwordLegal: - - "True": - message: "The password cannot match your username" - groups: [Strict] - properties: - username: - - NotBlank: ~ - password: - - NotBlank: ~ - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - User - Strict - - - - - .. code-block:: php - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class User - { - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('username', new Assert\NotBlank()); - $metadata->addPropertyConstraint('password', new Assert\NotBlank()); - - $metadata->addGetterConstraint('passwordLegal', new Assert\True(array( - 'message' => 'The password cannot match your first name', - 'groups' => array('Strict'), - ))); - - $metadata->setGroupSequence(array('User', 'Strict')); - } - } - -In this example, it will first validate all constraints in the group ``User`` -(which is the same as the ``Default`` group). Only if all constraints in -that group are valid, the second group, ``Strict``, will be validated. - -.. caution:: - - As you have already seen in the previous section, the ``Default`` group - and the group containing the class name (e.g. ``User``) were identical. - However, when using Group Sequences, they are no longer identical. The - ``Default`` group will now reference the group sequence, instead of all - constraints that do not belong to any group. - - This means that you have to use the ``{ClassName}`` (e.g. ``User``) group - when specifying a group sequence. When using ``Default``, you get an - infinite recursion (as the ``Default`` group references the group - sequence, which will contain the ``Default`` group which references the - same group sequence, ...). - -Group Sequence Providers -~~~~~~~~~~~~~~~~~~~~~~~~ - -Imagine a ``User`` entity which can be a normal user or a premium user. When -it's a premium user, some extra constraints should be added to the user entity -(e.g. the credit card details). To dynamically determine which groups should -be activated, you can create a Group Sequence Provider. First, create the -entity and a new constraint group called ``Premium``: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - - class User - { - /** - * @Assert\NotBlank() - */ - private $name; - - /** - * @Assert\CardScheme( - * schemes={"VISA"}, - * groups={"Premium"}, - * ) - */ - private $creditCard; - - // ... - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\User: - properties: - name: - - NotBlank: ~ - creditCard: - - CardScheme: - schemes: [VISA] - groups: [Premium] - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - use Symfony\Component\Validator\Constraints as Assert; - use Symfony\Component\Validator\Mapping\ClassMetadata; - - class User - { - private $name; - private $creditCard; - - // ... - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('name', new Assert\NotBlank()); - $metadata->addPropertyConstraint('creditCard', new Assert\CardScheme( - 'schemes' => array('VISA'), - 'groups' => array('Premium'), - )); - } - } - -Now, change the ``User`` class to implement -:class:`Symfony\\Component\\Validator\\GroupSequenceProviderInterface` and -add the -:method:`Symfony\\Component\\Validator\\GroupSequenceProviderInterface::getGroupSequence`, -method, which should return an array of groups to use:: - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - // ... - use Symfony\Component\Validator\GroupSequenceProviderInterface; - - class User implements GroupSequenceProviderInterface - { - // ... - - public function getGroupSequence() - { - $groups = array('User'); - - if ($this->isPremium()) { - $groups[] = 'Premium'; - } - - return $groups; - } - } - -At last, you have to notify the Validator component that your ``User`` class -provides a sequence of groups to be validated: - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - // ... - - /** - * @Assert\GroupSequenceProvider - */ - class User implements GroupSequenceProviderInterface - { - // ... - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\User: - group_sequence_provider: true - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/User.php - namespace AppBundle\Entity; - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - - class User implements GroupSequenceProviderInterface - { - // ... - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->setGroupSequenceProvider(true); - // ... - } - } - -.. _book-validation-raw-values: - -Validating Values and Arrays ----------------------------- - -So far, you've seen how you can validate entire objects. But sometimes, you -just want to validate a simple value - like to verify that a string is a valid -email address. This is actually pretty easy to do. From inside a controller, -it looks like this:: - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - // ... - public function addEmailAction($email) - { - $emailConstraint = new Assert\Email(); - // all constraint "options" can be set this way - $emailConstraint->message = 'Invalid email address'; - - // use the validator to validate the value - // If you're using the new 2.5 validation API (you probably are!) - $errorList = $this->get('validator')->validate( - $email, - $emailConstraint - ); - - // If you're using the old 2.4 validation API - /* - $errorList = $this->get('validator')->validateValue( - $email, - $emailConstraint - ); - */ - - if (0 === count($errorList)) { - // ... this IS a valid email address, do something - } else { - // this is *not* a valid email address - $errorMessage = $errorList[0]->getMessage(); - - // ... do something with the error - } - - // ... - } - -By calling ``validate`` on the validator, you can pass in a raw value and -the constraint object that you want to validate that value against. A full -list of the available constraints - as well as the full class name for each -constraint - is available in the :doc:`constraints reference ` -section. - -The ``validate`` method returns a :class:`Symfony\\Component\\Validator\\ConstraintViolationList` -object, which acts just like an array of errors. Each error in the collection -is a :class:`Symfony\\Component\\Validator\\ConstraintViolation` object, -which holds the error message on its ``getMessage`` method. - -Final Thoughts --------------- - -The Symfony ``validator`` is a powerful tool that can be leveraged to -guarantee that the data of any object is "valid". The power behind validation -lies in "constraints", which are rules that you can apply to properties or -getter methods of your object. And while you'll most commonly use the validation -framework indirectly when using forms, remember that it can be used anywhere -to validate any object. - -Learn more from the Cookbook ----------------------------- - -* :doc:`/cookbook/validation/custom_constraint` - -.. _Validator: https://github.com/symfony/Validator -.. _JSR303 Bean Validation specification: http://jcp.org/en/jsr/detail?id=303 diff --git a/book/bundles.rst b/bundles.rst similarity index 71% rename from book/bundles.rst rename to bundles.rst index ff09cc6fc3e..27481a237c6 100644 --- a/book/bundles.rst +++ b/bundles.rst @@ -16,8 +16,8 @@ in your application and to optimize them the way you want. .. note:: - While you'll learn the basics here, an entire cookbook entry is devoted - to the organization and best practices of :doc:`bundles `. + While you'll learn the basics here, an entire article is devoted to the + organization and best practices of :doc:`bundles `. A bundle is simply a structured set of files within a directory that implement a single feature. You might create a BlogBundle, a ForumBundle or @@ -59,23 +59,20 @@ are used by your application (including the core Symfony bundles). .. tip:: - A bundle can live *anywhere* as long as it can be autoloaded (via the - autoloader configured at ``app/autoload.php``). + A bundle can live *anywhere* as long as it can be autoloaded (via the + autoloader configured at ``app/autoload.php``). Creating a Bundle ----------------- -The Symfony Standard Edition comes with a handy task that creates a fully-functional -bundle for you. Of course, creating a bundle by hand is pretty easy as well. +`SensioGeneratorBundle`_ is an optional bundle that includes commands to create +different elements of your application, such as bundles. If you create lots of +bundles, consider using it. However, this section creates and enables a new +bundle by hand to show how simple it is to do it. -To show you how simple the bundle system is, create a new bundle called -AcmeTestBundle and enable it. - -.. tip:: - - The ``Acme`` portion is just a dummy name that should be replaced by - some "vendor" name that represents you or your organization (e.g. - ABCTestBundle for some company named ``ABC``). +The new bundle is called AcmeTestBundle, where the ``Acme`` portion is just a +dummy name that should be replaced by some "vendor" name that represents you or +your organization (e.g. ABCTestBundle for some company named ``ABC``). Start by creating a ``src/Acme/TestBundle/`` directory and adding a new file called ``AcmeTestBundle.php``:: @@ -91,10 +88,10 @@ called ``AcmeTestBundle.php``:: .. tip:: - The name AcmeTestBundle follows the standard - :ref:`Bundle naming conventions `. You could - also choose to shorten the name of the bundle to simply TestBundle by naming - this class TestBundle (and naming the file ``TestBundle.php``). + The name AcmeTestBundle follows the standard + :ref:`Bundle naming conventions `. You could + also choose to shorten the name of the bundle to simply TestBundle by naming + this class TestBundle (and naming the file ``TestBundle.php``). This empty class is the only piece you need to create the new bundle. Though commonly empty, this class is powerful and can be used to customize the behavior @@ -120,7 +117,7 @@ And while it doesn't do anything yet, AcmeTestBundle is now ready to be used. And as easy as this is, Symfony also provides a command-line interface for generating a basic bundle skeleton: -.. code-block:: bash +.. code-block:: terminal $ php app/console generate:bundle --namespace=Acme/TestBundle @@ -130,9 +127,9 @@ tools later. .. tip:: - Whenever creating a new bundle or using a third-party bundle, always make - sure the bundle has been enabled in ``registerBundles()``. When using - the ``generate:bundle`` command, this is done for you. + Whenever creating a new bundle or using a third-party bundle, always make + sure the bundle has been enabled in ``registerBundles()``. When using + the ``generate:bundle`` command, this is done for you. Bundle Directory Structure -------------------------- @@ -154,7 +151,7 @@ of the most common elements of a bundle: Houses configuration, including routing configuration (e.g. ``routing.yml``). ``Resources/views/`` - Holds templates organized by controller name (e.g. ``Hello/index.html.twig``). + Holds templates organized by controller name (e.g. ``Random/index.html.twig``). ``Resources/public/`` Contains web assets (images, stylesheets, etc) and is copied or symbolically @@ -167,9 +164,19 @@ of the most common elements of a bundle: A bundle can be as small or large as the feature it implements. It contains only the files you need and nothing else. -As you move through the book, you'll learn how to persist objects to a database, -create and validate forms, create translations for your application, write -tests and much more. Each of these has their own place and role within the -bundle. +As you move through the guides, you'll learn how to persist objects to a +database, create and validate forms, create translations for your application, +write tests and much more. Each of these has their own place and role within +the bundle. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + bundles/* -_`third-party bundles`: http://knpbundles.com +.. _`third-party bundles`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories +.. _`SensioGeneratorBundle`: https://symfony.com/doc/current/bundles/SensioGeneratorBundle/index.html diff --git a/cookbook/bundles/best_practices.rst b/bundles/best_practices.rst similarity index 71% rename from cookbook/bundles/best_practices.rst rename to bundles/best_practices.rst index 61b6fdfe6e1..3fa8ae25bb5 100644 --- a/cookbook/bundles/best_practices.rst +++ b/bundles/best_practices.rst @@ -13,7 +13,7 @@ This article is all about how to structure your **reusable bundles** so that they're easy to configure and extend. Many of these recommendations do not apply to application bundles because you'll want to keep those as simple as possible. For application bundles, just follow the practices shown throughout -the book and cookbook. +the guides. .. seealso:: @@ -31,13 +31,13 @@ Bundle Name A bundle is also a PHP namespace. The namespace must follow the `PSR-0`_ or `PSR-4`_ interoperability standards for PHP namespaces and class names: it starts with a vendor segment, followed by zero or more category segments, and it ends -with the namespace short name, which must end with a ``Bundle`` suffix. +with the namespace short name, which must end with ``Bundle``. A namespace becomes a bundle as soon as you add a bundle class to it. The bundle class name must follow these simple rules: * Use only alphanumeric characters and underscores; -* Use a CamelCased name; +* Use a StudlyCaps name (i.e. camelCase with the first letter uppercased); * Use a descriptive and short name (no more than two words); * Prefix the name with the concatenation of the vendor (and optionally the category namespaces); @@ -48,8 +48,8 @@ Here are some valid bundle namespaces and class names: ========================== ================== Namespace Bundle Class Name ========================== ================== -``Acme\Bundle\BlogBundle`` ``AcmeBlogBundle`` -``Acme\BlogBundle`` ``AcmeBlogBundle`` +``Acme\Bundle\BlogBundle`` AcmeBlogBundle +``Acme\BlogBundle`` AcmeBlogBundle ========================== ================== By convention, the ``getName()`` method of the bundle class should return the @@ -58,8 +58,7 @@ class name. .. note:: If you share your bundle publicly, you must use the bundle class name as - the name of the repository (``AcmeBlogBundle`` and not ``BlogBundle`` - for instance). + the name of the repository (AcmeBlogBundle and not BlogBundle for instance). .. note:: @@ -68,7 +67,7 @@ class name. :class:`Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle`. Each bundle has an alias, which is the lower-cased short version of the bundle -name using underscores (``acme_blog`` for ``AcmeBlogBundle``). This alias +name using underscores (``acme_blog`` for AcmeBlogBundle). This alias is used to enforce uniqueness within a project and for defining bundle's configuration options (see below for some usage examples). @@ -83,9 +82,8 @@ The basic directory structure of an AcmeBlogBundle must read as follows: ├─ AcmeBlogBundle.php ├─ Controller/ ├─ README.md + ├─ LICENSE ├─ Resources/ - │ ├─ meta/ - │ │ └─ LICENSE │ ├─ config/ │ ├─ doc/ │ │ └─ index.rst @@ -102,51 +100,51 @@ that automated tools can rely on: * ``README.md``: This file contains the basic description of the bundle and it usually shows some basic examples and links to its full documentation (it can use any of the markup formats supported by GitHub, such as ``README.rst``); -* ``Resources/meta/LICENSE``: The full license for the code. The license file - can also be stored in the bundle's root directory to follow the generic - conventions about packages; +* ``LICENSE``: The full contents of the license used by the code. Most third-party + bundles are published under the MIT license, but you can `choose any license`_; * ``Resources/doc/index.rst``: The root file for the Bundle documentation. -The depth of sub-directories should be kept to the minimum for most used -classes and files (two levels maximum). +The depth of subdirectories should be kept to a minimum for the most used +classes and files. Two levels is the maximum. The bundle directory is read-only. If you need to write temporary files, store them under the ``cache/`` or ``log/`` directory of the host application. Tools can generate files in the bundle directory structure, but only if the generated files are going to be part of the repository. -The following classes and files have specific emplacements: - -=============================== ============================= -Type Directory -=============================== ============================= -Commands ``Command/`` -Controllers ``Controller/`` -Service Container Extensions ``DependencyInjection/`` -Event Listeners ``EventListener/`` -Model classes [1] ``Model/`` -Configuration ``Resources/config/`` -Web Resources (CSS, JS, images) ``Resources/public/`` -Translation files ``Resources/translations/`` -Templates ``Resources/views/`` -Unit and Functional Tests ``Tests/`` -=============================== ============================= - -[1] See :doc:`/cookbook/doctrine/mapping_model_classes` for how to handle the -mapping with a compiler pass. +The following classes and files have specific emplacements (some are mandatory +and others are just conventions followed by most developers): + +=================================================== ======================================== +Type Directory +=================================================== ======================================== +Commands ``Command/`` +Controllers ``Controller/`` +Service Container Extensions ``DependencyInjection/`` +Doctrine ORM entities (when not using annotations) ``Entity/`` +Doctrine ODM documents (when not using annotations) ``Document/`` +Event Listeners ``EventListener/`` +Configuration ``Resources/config/`` +Web Resources (CSS, JS, images) ``Resources/public/`` +Translation files ``Resources/translations/`` +Validation (when not using annotations) ``Resources/config/validation/`` +Serialization (when not using annotations) ``Resources/config/serialization/`` +Templates ``Resources/views/`` +Unit and Functional Tests ``Tests/`` +=================================================== ======================================== Classes ------- The bundle directory structure is used as the namespace hierarchy. For -instance, a ``ContentController`` controller is stored in -``Acme/BlogBundle/Controller/ContentController.php`` and the fully qualified -class name is ``Acme\BlogBundle\Controller\ContentController``. +instance, a ``ContentController`` controller which is stored in +``Acme/BlogBundle/Controller/ContentController.php`` would have the fully +qualified class name of ``Acme\BlogBundle\Controller\ContentController``. All classes and files must follow the :doc:`Symfony coding standards `. Some classes should be seen as facades and should be as short as possible, like -Commands, Helpers, Listeners, and Controllers. +Commands, Helpers, Listeners and Controllers. Classes that connect to the event dispatcher should be suffixed with ``Listener``. @@ -159,8 +157,8 @@ Vendors A bundle must not embed third-party PHP libraries. It should rely on the standard Symfony autoloading instead. -A bundle should not embed third-party libraries written in JavaScript, CSS, or -any other language. +A bundle should also not embed third-party libraries written in JavaScript, +CSS or any other language. Tests ----- @@ -175,18 +173,22 @@ the ``Tests/`` directory. Tests should follow the following principles: * The tests should cover at least 95% of the code base. .. note:: - A test suite must not contain ``AllTests.php`` scripts, but must rely on the - existence of a ``phpunit.xml.dist`` file. + + A test suite must not contain ``AllTests.php`` scripts, but must rely on the + existence of a ``phpunit.xml.dist`` file. Documentation ------------- All classes and functions must come with full PHPDoc. -Extensive documentation should also be provided in the -:doc:`reStructuredText ` format, under -the ``Resources/doc/`` directory; the ``Resources/doc/index.rst`` file is -the only mandatory file and must be the entry point for the documentation. +Extensive documentation should also be provided in the ``Resources/doc/`` +directory. +The index file (for example ``Resources/doc/index.rst`` or +``Resources/doc/index.md``) is the only mandatory file and must be the entry +point for the documentation. The +:doc:`reStructuredText (rST) ` is the format +used to render the documentation on symfony.com. Installation Instructions ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -207,7 +209,7 @@ following standardized instructions in your ``README.md`` file. Open a command console, enter your project directory and execute the following command to download the latest stable version of this bundle: - ```bash + ```console $ composer require "~1" ``` @@ -232,7 +234,6 @@ following standardized instructions in your ``README.md`` file. { $bundles = array( // ... - new \\(), ); @@ -254,7 +255,7 @@ following standardized instructions in your ``README.md`` file. Open a command console, enter your project directory and execute the following command to download the latest stable version of this bundle: - .. code-block:: bash + .. code-block:: terminal $ composer require "~1" @@ -343,14 +344,22 @@ The end user can provide values in any configuration file: # app/config/config.yml parameters: - acme_blog.author.email: fabien@example.com + acme_blog.author.email: 'fabien@example.com' .. code-block:: xml - - fabien@example.com - + + + + + fabien@example.com + + + .. code-block:: php @@ -362,7 +371,7 @@ Retrieve the configuration parameters in your code from the container:: $container->getParameter('acme_blog.author.email'); Even if this mechanism is simple enough, you should consider using the more -advanced :doc:`semantic bundle configuration `. +advanced :doc:`semantic bundle configuration `. Versioning ---------- @@ -381,7 +390,7 @@ be :ref:`defined as private `. .. seealso:: You can learn much more about service loading in bundles reading this article: - :doc:`How to Load Service Configuration inside a Bundle `. + :doc:`How to Load Service Configuration inside a Bundle `. Composer Metadata ----------------- @@ -391,9 +400,9 @@ The ``composer.json`` file should include at least the following metadata: ``name`` Consists of the vendor and the short bundle name. If you are releasing the bundle on your own instead of on behalf of a company, use your personal name - (e.g. ``johnsmith/blog-bundle``). The bundle short name excludes the vendor - name and separates each word with an hyphen. For example: ``AcmeBlogBundle`` - is transformed into ``blog-bundle`` and ``AcmeSocialConnectBundle`` is + (e.g. ``johnsmith/blog-bundle``). Exclude the vendor name from the bundle + short name and separate each word with an hyphen. For example: AcmeBlogBundle + is transformed into ``blog-bundle`` and AcmeSocialConnectBundle is transformed into ``social-connect-bundle``. ``description`` @@ -403,8 +412,7 @@ The ``composer.json`` file should include at least the following metadata: Use the ``symfony-bundle`` value. ``license`` - ``MIT`` is the preferred license for Symfony bundles, but you can use any - other license. + a string (or array of strings) with a `valid license identifier`_, such as ``MIT``. ``autoload`` This information is used by Symfony to load the classes of the bundle. The @@ -463,12 +471,30 @@ API is being used. The following code, would work for *all* users:: } } -Learn more from the Cookbook ----------------------------- +Resources +--------- + +If the bundle references any resources (config files, translation files, etc.), +don't use physical paths (e.g. ``__DIR__/config/services.xml``) but logical +paths (e.g. ``@AppBundle/Resources/config/services.xml``). + +The logical paths are required because of the bundle overriding mechanism that +lets you override any resource/file of any bundle. See :ref:`http-kernel-resource-locator` +for more details about transforming physical paths into logical paths. + +Beware that templates use a simplified version of the logical path shown above. +For example, an ``index.html.twig`` template located in the ``Resources/views/Default/`` +directory of the AppBundle, is referenced as ``@App/Default/index.html.twig``. + +Learn more +---------- -* :doc:`/cookbook/bundles/extension` +* :doc:`/bundles/extension` +* :doc:`/bundles/configuration` -.. _`PSR-0`: http://www.php-fig.org/psr/psr-0/ -.. _`PSR-4`: http://www.php-fig.org/psr/psr-4/ -.. _`Semantic Versioning Standard`: http://semver.org/ +.. _`PSR-0`: https://www.php-fig.org/psr/psr-0/ +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ +.. _`Semantic Versioning Standard`: https://semver.org/ .. _`Packagist`: https://packagist.org/ +.. _`choose any license`: https://choosealicense.com/ +.. _`valid license identifier`: https://spdx.org/licenses/ diff --git a/cookbook/bundles/configuration.rst b/bundles/configuration.rst similarity index 82% rename from cookbook/bundles/configuration.rst rename to bundles/configuration.rst index f04f1018ccf..d155ea86022 100644 --- a/cookbook/bundles/configuration.rst +++ b/bundles/configuration.rst @@ -75,20 +75,20 @@ bundle configuration would look like: acme_social: twitter: client_id: 123 - client_secret: $ecret + client_secret: your_secret .. code-block:: xml - - + @@ -99,12 +99,12 @@ bundle configuration would look like: // app/config/config.php $container->loadFromExtension('acme_social', array( 'client_id' => 123, - 'client_secret' => '$ecret', + 'client_secret' => 'your_secret', )); .. seealso:: - Read more about the extension in :doc:`/cookbook/bundles/extension`. + Read more about the extension in :doc:`/bundles/extension`. .. tip:: @@ -118,14 +118,13 @@ bundle configuration would look like: .. seealso:: For parameter handling within a dependency injection container see - :doc:`/cookbook/configuration/using_parameters_in_dic`. - + :doc:`/configuration/using_parameters_in_dic`. Processing the ``$configs`` Array ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First things first, you have to create an extension class as explained in -:doc:`extension`. +:doc:`/bundles/extension`. Whenever a user includes the ``acme_social`` key (which is the DI alias) in a configuration file, the configuration under it is added to an array of @@ -139,7 +138,7 @@ For the configuration example in the previous section, the array passed to your array( 'twitter' => array( 'client_id' => 123, - 'client_secret' => '$ecret', + 'client_secret' => 'your_secret', ), ), ) @@ -155,7 +154,7 @@ beneath it, the incoming array might look like this:: array( 'twitter' => array( 'client_id' => 123, - 'client_secret' => '$secret', + 'client_secret' => 'your_secret', ), ), // values from config_dev.yml @@ -218,18 +217,63 @@ This class can now be used in your ``load()`` method to merge configurations and force validation (e.g. if an additional option was passed, an exception will be thrown):: + // src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php + public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - // ... + + // you now have these 2 config keys + // $config['twitter']['client_id'] and $config['twitter']['client_secret'] } The ``processConfiguration()`` method uses the configuration tree you've defined in the ``Configuration`` class to validate, normalize and merge all the configuration arrays together. +Now, you can use the ``$config`` variable to modify a service provided by your bundle. +For example, imagine your bundle has the following example config: + +.. code-block:: xml + + + + + + + + + + + + + +In your extension, you can load this and dynamically set its arguments:: + + // src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php + // ... + + use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; + use Symfony\Component\Config\FileLocator; + + public function load(array $configs, ContainerBuilder $container) + { + $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config')); + $loader->load('services.xml'); + + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $definition = $container->getDefinition('acme.social.twitter_client'); + $definition->replaceArgument(0, $config['twitter']['client_id']); + $definition->replaceArgument(1, $config['twitter']['client_secret']); + } + .. tip:: Instead of calling ``processConfiguration()`` in your extension each time you @@ -253,9 +297,7 @@ configuration arrays together. } This class uses the ``getConfiguration()`` method to get the Configuration - instance. You should override it if your Configuration class is not called - ``Configuration`` or if it is not placed in the same namespace as the - extension. + instance. .. sidebar:: Processing the Configuration yourself @@ -285,7 +327,7 @@ to allow one ``Extension`` class to modify the configuration passed to another bundle's ``Extension`` class, as if the end-developer has actually placed that configuration in their ``app/config/config.yml`` file. This can be achieved using a prepend extension. For more details, see -:doc:`/cookbook/bundles/prepend_extension`. +:doc:`/bundles/prepend_extension`. Dump the Configuration ---------------------- @@ -294,8 +336,8 @@ The ``config:dump-reference`` command dumps the default configuration of a bundle in the console using the Yaml format. As long as your bundle's configuration is located in the standard location -(``YourBundle\DependencyInjection\Configuration``) and does not require -arguments to be passed to the constructor it will work automatically. If you +(``YourBundle\DependencyInjection\Configuration``) and does not have +a constructor it will work automatically. If you have something different, your ``Extension`` class must override the :method:`Extension::getConfiguration() ` method and return an instance of your ``Configuration``. @@ -325,7 +367,7 @@ configuration of a specific bundle. The namespace is returned from the :method:`Extension::getNamespace() ` method. By convention, the namespace is a URL (it doesn't have to be a valid URL nor does it need to exists). By default, the namespace for a bundle is -``http://example.org/dic/schema/DI_ALIAS``, where ``DI_ALIAS`` is the DI alias of +``http://example.org/schema/dic/DI_ALIAS``, where ``DI_ALIAS`` is the DI alias of the extension. You might want to change this to a more professional URL:: // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php @@ -353,7 +395,7 @@ In order to use the schema, the XML configuration file must provide an ``xsi:schemaLocation`` attribute pointing to the XSD file for a certain XML namespace. This location always starts with the XML namespace. This XML namespace is then replaced with the XSD validation base path returned from -:method:`Extension::getXsdValidationBasePath() ` +:method:`Extension::getXsdValidationBasePath() ` method. This namespace is then followed by the rest of the path from the base path to the file itself. @@ -373,14 +415,13 @@ can place it anywhere you like. You should return this path as the base path:: } } -Assume the XSD file is called ``hello-1.0.xsd``, the schema location will be +Assuming the XSD file is called ``hello-1.0.xsd``, the schema location will be ``http://acme_company.com/schema/dic/hello/hello-1.0.xsd``: .. code-block:: xml - addClassesToCompile(array( + UserManager::class, + Slugger::class, + // ... + )); + } + +.. note:: + + If some class extends from other classes, all its parents are automatically + included in the list of classes to compile. + +Beware that this technique **can't be used in some cases**: + +* When classes contain annotations, such as controllers with ``@Route`` + annotations and entities with ``@ORM`` or ``@Assert`` annotations, because + the file location retrieved from PHP reflection changes; +* When classes use the ``__DIR__`` and ``__FILE__`` constants, because their + values will change when loading these classes from the ``classes.php`` file. diff --git a/cookbook/bundles/index.rst b/bundles/index.rst similarity index 100% rename from cookbook/bundles/index.rst rename to bundles/index.rst diff --git a/cookbook/bundles/inheritance.rst b/bundles/inheritance.rst similarity index 78% rename from cookbook/bundles/inheritance.rst rename to bundles/inheritance.rst index 90457f54320..f828ad90f45 100644 --- a/cookbook/bundles/inheritance.rst +++ b/bundles/inheritance.rst @@ -10,11 +10,12 @@ in one of your own bundles. Symfony gives you a very convenient way to override things like controllers, templates, and other files in a bundle's ``Resources/`` directory. -For example, suppose that you're installing the `FOSUserBundle`_, but you -want to override its base ``layout.html.twig`` template, as well as one of -its controllers. Suppose also that you have your own UserBundle where you want -the overridden files to live. Start by registering the FOSUserBundle as the -"parent" of your bundle:: +For example, suppose that you have installed `FOSUserBundle`_, but you want to +override its base ``layout.html.twig`` template, as well as one of its +controllers. + +First, create a new bundle called UserBundle and enable it in your application. +Then, register the third-party FOSUserBundle as the "parent" of your bundle:: // src/UserBundle/UserBundle.php namespace UserBundle; @@ -40,7 +41,7 @@ simply by creating a file with the same name. Overriding Controllers ~~~~~~~~~~~~~~~~~~~~~~ -Suppose you want to add some functionality to the ``registerAction`` of a +Suppose you want to add some functionality to the ``registerAction()`` of a ``RegistrationController`` that lives inside FOSUserBundle. To do so, just create your own ``RegistrationController.php`` file, override the bundle's original method, and change its functionality:: @@ -92,14 +93,15 @@ The same goes for routing files and some other resources. The overriding of resources only works when you refer to resources with the ``@FOSUserBundle/Resources/config/routing/security.xml`` method. - If you refer to resources without using the ``@BundleName`` shortcut, they - can't be overridden in this way. + You need to use the ``@BundleName`` shortcut when referring to resources + so they can be successfully overridden (except templates, which are + overridden in a different way, as explained in :doc:`/templating/overriding`). .. caution:: - Translation and validation files do not work in the same way as described - above. Read ":ref:`override-translations`" if you want to learn how to - override translations and see ":ref:`override-validation`" for tricks to - override the validation. + Translation and validation files do not work in the same way as described + above. Read ":ref:`override-translations`" if you want to learn how to + override translations and see ":ref:`override-validation`" for tricks to + override the validation. .. _`FOSUserBundle`: https://github.com/friendsofsymfony/fosuserbundle diff --git a/cookbook/bundles/installation.rst b/bundles/installation.rst similarity index 86% rename from cookbook/bundles/installation.rst rename to bundles/installation.rst index 1dd7e1f4d83..a0e71df54e5 100644 --- a/cookbook/bundles/installation.rst +++ b/bundles/installation.rst @@ -26,15 +26,14 @@ the bundle on the `Packagist.org`_ site. .. tip:: - Looking for bundles? Try searching at `KnpBundles.com`_: the unofficial - archive of Symfony Bundles. + Looking for bundles? Try searching for `symfony-bundle topic on GitHub`_. 2) Install the Bundle via Composer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now that you know the package name, you can install it via Composer: -.. code-block:: bash +.. code-block:: terminal $ composer require friendsofsymfony/user-bundle @@ -42,14 +41,14 @@ This will choose the best version for your project, add it to ``composer.json`` and download its code into the ``vendor/`` directory. If you need a specific version, include it as the second argument of the `composer require`_ command: -.. code-block:: bash +.. code-block:: terminal $ composer require friendsofsymfony/user-bundle "~2.0" B) Enable the Bundle -------------------- -At this point, the bundle is installed in your Symfony project (in +At this point, the bundle is installed in your Symfony project (e.g. ``vendor/friendsofsymfony/``) and the autoloader recognizes its classes. The only thing you need to do now is register the bundle in ``AppKernel``:: @@ -72,7 +71,7 @@ The only thing you need to do now is register the bundle in ``AppKernel``:: } In a few rare cases, you may want a bundle to be *only* enabled in the development -:doc:`environment `. For example, +:doc:`environment `. For example, the DoctrineFixturesBundle helps to load dummy data - something you probably only want to do while developing. To only load this bundle in the ``dev`` and ``test`` environments, register the bundle in this way:: @@ -106,28 +105,28 @@ in ``app/config/config.yml``. The bundle's documentation will tell you about the configuration, but you can also get a reference of the bundle's configuration via the ``config:dump-reference`` command: -.. code-block:: bash +.. code-block:: terminal $ app/console config:dump-reference AsseticBundle Instead of the full bundle name, you can also pass the short name used as the root of the bundle's configuration: -.. code-block:: bash +.. code-block:: terminal $ app/console config:dump-reference assetic The output will look like this: -.. code-block:: text +.. code-block:: yaml assetic: - debug: %kernel.debug% + debug: '%kernel.debug%' use_controller: - enabled: %kernel.debug% + enabled: '%kernel.debug%' profiler: false - read_from: %kernel.root_dir%/../web - write_to: %assetic.read_from% + read_from: '%kernel.root_dir%/../web' + write_to: '%assetic.read_from%' java: /usr/bin/java node: /usr/local/bin/node node_paths: [] @@ -142,5 +141,5 @@ what to do next. Have fun! .. _their documentation: https://getcomposer.org/doc/00-intro.md .. _Packagist.org: https://packagist.org .. _FOSUserBundle: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _KnpBundles.com: http://knpbundles.com/ .. _`composer require`: https://getcomposer.org/doc/03-cli.md#require +.. _`symfony-bundle topic on GitHub`: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories diff --git a/cookbook/bundles/override.rst b/bundles/override.rst similarity index 58% rename from cookbook/bundles/override.rst rename to bundles/override.rst index db60e778bba..52950a29c3c 100644 --- a/cookbook/bundles/override.rst +++ b/bundles/override.rst @@ -7,13 +7,21 @@ How to Override any Part of a Bundle This document is a quick reference for how to override different parts of third-party bundles. +.. tip:: + + The bundle overriding mechanism means that you cannot use physical paths to + refer to bundle's resources (e.g. ``__DIR__/config/services.xml``). Always + use logical paths in your bundles (e.g. ``@AppBundle/Resources/config/services.xml``) + and call the :ref:`locateResource() method ` + to turn them into physical paths when needed. + Templates --------- For information on overriding templates, see -* :ref:`overriding-bundle-templates`. -* :doc:`/cookbook/bundles/inheritance` +* :doc:`/templating/overriding`. +* :doc:`/bundles/inheritance` Routing ------- @@ -31,49 +39,21 @@ Controllers Assuming the third-party bundle involved uses non-service controllers (which is almost always the case), you can easily override controllers via bundle -inheritance. For more information, see :doc:`/cookbook/bundles/inheritance`. +inheritance. For more information, see :doc:`/bundles/inheritance`. If the controller is a service, see the next section on how to override it. Services & Configuration ------------------------ -In order to override/extend a service, there are two options. First, you can -set the parameter holding the service's class name to your own class by setting -it in ``app/config/config.yml``. This of course is only possible if the class name is -defined as a parameter in the service config of the bundle containing the -service. For example, to override the class used for Symfony's ``translator`` -service, you would override the ``translator.class`` parameter. Knowing exactly -which parameter to override may take some research. For the translator, the -parameter is defined and used in the ``Resources/config/translation.xml`` file -in the core FrameworkBundle: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - parameters: - translator.class: Acme\HelloBundle\Translation\Translator - - .. code-block:: xml - - - - Acme\HelloBundle\Translation\Translator - - - .. code-block:: php - - // app/config/config.php - $container->setParameter('translator.class', 'Acme\HelloBundle\Translation\Translator'); - -Secondly, if the class is not available as a parameter, you want to make sure the -class is always overridden when your bundle is used or if you need to modify -something beyond just the class name, you should use a compiler pass:: +If you want to modify service definitions of another bundle, you can use a compiler +pass to change the class of the service or to modify method calls. In the following +example, the implementing class for the ``original-service-id`` is changed to +``Acme\DemoBundle\YourService``:: // src/Acme/DemoBundle/DependencyInjection/Compiler/OverrideServiceCompilerPass.php namespace Acme\DemoBundle\DependencyInjection\Compiler; + use Acme\DemoBundle\YourService; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -82,16 +62,11 @@ something beyond just the class name, you should use a compiler pass:: public function process(ContainerBuilder $container) { $definition = $container->getDefinition('original-service-id'); - $definition->setClass('Acme\DemoBundle\YourService'); + $definition->setClass(YourService::class); } } -In this example you fetch the service definition of the original service, and set -its class name to your own class. - -See :doc:`/cookbook/service_container/compiler_passes` for information on how to use -compiler passes. If you want to do something beyond just overriding the class, -like adding a method call, you can only use the compiler pass method. +For more information on compiler passes, see :doc:`/service_container/compiler_passes`. Entities & Entity Mapping ------------------------- @@ -105,17 +80,8 @@ associations. Learn more about this feature and its limitations in Forms ----- -In order to override a form type, it has to be registered as a service (meaning -it is tagged as ``form.type``). You can then override it as you would override any -service as explained in `Services & Configuration`_. This, of course, will only -work if the type is referred to by its alias rather than being instantiated, -e.g.:: - - $builder->add('name', 'custom_type'); - -rather than:: - - $builder->add('name', new CustomType()); +Existing form types can be modified defining +:doc:`form type extensions `. .. _override-validation: @@ -126,10 +92,10 @@ Symfony loads all validation configuration files from every bundle and combines them into one validation metadata tree. This means you are able to add new constraints to a property, but you cannot override them. -To override this, the 3rd party bundle needs to have configuration for -:ref:`validation groups `. For instance, -the FOSUserBundle has this configuration. To create your own validation, add -the constraints to a new validation group: +To overcome this, the 3rd party bundle needs to have configuration for +:doc:`validation groups `. For instance, the FOSUserBundle +has this configuration. To create your own validation, add the constraints +to a new validation group: .. configuration-block:: @@ -188,14 +154,11 @@ can override the translations from any translation file, as long as it is in .. caution:: - The last translation file always wins. That means that you need to make - sure that the bundle containing *your* translations is loaded after any + Translation files are not aware of :doc:`bundle inheritance `. + If you want to override translations from the parent bundle or another bundle, + make sure that the bundle containing *your* translations is loaded after any bundle whose translations you're overriding. This is done in ``AppKernel``. - Translation files are also not aware of :doc:`bundle inheritance `. - If you want to override translations from the parent bundle, be sure that the - parent bundle is loaded before the child bundle in the ``AppKernel`` class. - - The file that always wins is the one that is placed in - ``app/Resources/translations``, as those files are always loaded last. + Finally, translations located in ``app/Resources/translations`` will override + all the other translations since those files are always loaded last. .. _`the Doctrine documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#overrides diff --git a/cookbook/bundles/prepend_extension.rst b/bundles/prepend_extension.rst similarity index 75% rename from cookbook/bundles/prepend_extension.rst rename to bundles/prepend_extension.rst index f4a0aff3cc9..28610a27154 100644 --- a/cookbook/bundles/prepend_extension.rst +++ b/bundles/prepend_extension.rst @@ -2,7 +2,7 @@ single: Configuration; Semantic single: Bundle; Extension configuration -How to Simplify Configuration of multiple Bundles +How to Simplify Configuration of Multiple Bundles ================================================= When building reusable and extensible applications, developers are often @@ -12,9 +12,9 @@ users to choose to remove functionality they are not using. Creating multiple bundles has the drawback that configuration becomes more tedious and settings often need to be repeated for various bundles. -Using the below approach, it is possible to remove the disadvantage of the -multiple bundle approach by enabling a single Extension to prepend the settings -for any bundle. It can use the settings defined in the ``app/config/config.yml`` +It is possible to remove the disadvantage of the multiple bundle approach +by enabling a single Extension to prepend the settings for any bundle. +It can use the settings defined in the ``app/config/config.yml`` to prepend settings just as if they had been written explicitly by the user in the application configuration. @@ -56,6 +56,7 @@ The following example illustrates how to prepend a configuration setting in multiple bundles as well as disable a flag in multiple bundles in case a specific other bundle is not registered:: + // src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php public function prepend(ContainerBuilder $container) { // get all bundles @@ -69,8 +70,10 @@ in case a specific other bundle is not registered:: case 'acme_something': case 'acme_other': // set use_acme_goodbye to false in the config of - // acme_something and acme_other note that if the user manually - // configured use_acme_goodbye to true in the app/config/config.yml + // acme_something and acme_other + // + // note that if the user manually configured + // use_acme_goodbye to true in app/config/config.yml // then the setting would in the end be true and not false $container->prependExtensionConfig($name, $config); break; @@ -113,11 +116,21 @@ The above would be the equivalent of writing the following into the .. code-block:: xml - - non_default - + + + + + non_default + + + - + .. code-block:: php @@ -131,3 +144,10 @@ The above would be the equivalent of writing the following into the // ... 'use_acme_goodbye' => false, )); + +More than one Bundle using PrependExtensionInterface +---------------------------------------------------- + +If there is more than one bundle that prepends the same extension and defines +the same key, the bundle that is registered **first** will take priority: +next bundles won't override this specific config setting. diff --git a/cookbook/bundles/remove.rst b/bundles/remove.rst similarity index 63% rename from cookbook/bundles/remove.rst rename to bundles/remove.rst index 9956704cdd2..644e8742310 100644 --- a/cookbook/bundles/remove.rst +++ b/bundles/remove.rst @@ -1,25 +1,16 @@ .. index:: - single: Bundle; Removing AcmeDemoBundle + single: Bundle; Removing a bundle -How to Remove the AcmeDemoBundle -================================ - -The Symfony Standard Edition comes with a complete demo that lives inside a -bundle called AcmeDemoBundle. It is a great boilerplate to refer to while -starting a project, but you'll probably want to eventually remove it. - -.. tip:: - - This article uses the AcmeDemoBundle as an example, but you can use - these steps to remove any bundle. +How to Remove a Bundle +====================== 1. Unregister the Bundle in the ``AppKernel`` --------------------------------------------- To disconnect the bundle from the framework, you should remove the bundle from -the ``AppKernel::registerBundles()`` method. The bundle is normally found in -the ``$bundles`` array but the AcmeDemoBundle is only registered in the -development environment and you can find it inside the if statement below:: +the ``AppKernel::registerBundles()`` method. The bundle will likely be found in +the ``$bundles`` array declaration or added to it in a later statement if the +bundle is only registered in the development environment:: // app/AppKernel.php @@ -28,7 +19,9 @@ development environment and you can find it inside the if statement below:: { public function registerBundles() { - $bundles = array(...); + $bundles = array( + new Acme\DemoBundle\AcmeDemoBundle(), + ); if (in_array($this->getEnvironment(), array('dev', 'test'))) { // comment or remove this line: @@ -48,8 +41,9 @@ that refers to the bundle. 2.1 Remove Bundle Routing ~~~~~~~~~~~~~~~~~~~~~~~~~ -The routing for the AcmeDemoBundle can be found in ``app/config/routing_dev.yml``. -Remove the ``_acme_demo`` entry at the bottom of this file. +*Some* bundles require you to import routing configuration. Check for references +to the bundle in ``app/config/routing.yml`` and ``app/config/routing_dev.yml``. +If you find any references, remove them completely. 2.2 Remove Bundle Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -60,18 +54,13 @@ quickly spot bundle configuration by looking for an ``acme_demo`` (or whatever the name of the bundle is, e.g. ``fos_user`` for the FOSUserBundle) string in the configuration files. -The AcmeDemoBundle doesn't have configuration. However, the bundle is -used in the configuration for the ``app/config/security.yml`` file. You can -use it as a boilerplate for your own security, but you **can** also remove -everything: it doesn't matter to Symfony if you remove it or not. - 3. Remove the Bundle from the Filesystem ---------------------------------------- Now you have removed every reference to the bundle in your application, you -should remove the bundle from the filesystem. The bundle is located in the -``src/Acme/DemoBundle`` directory. You should remove this directory and you -can remove the ``Acme`` directory as well. +should remove the bundle from the filesystem. The bundle will be located in +`src/` for example the ``src/Acme/DemoBundle`` directory. You should remove this +directory, and any parent directories that are now empty (e.g. ``src/Acme/``). .. tip:: @@ -91,11 +80,6 @@ Remove the assets of the bundle in the web/ directory (e.g. 4. Remove Integration in other Bundles -------------------------------------- -.. note:: - - This doesn't apply to the AcmeDemoBundle - no other bundles depend - on it, so you can skip this step. - Some bundles rely on other bundles, if you remove one of the two, the other will probably not work. Be sure that no other bundles, third party or self-made, rely on the bundle you are about to remove. diff --git a/changelog.rst b/changelog.rst index 9444def5556..1bd1df701aa 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,6 +1,10 @@ .. index:: single: CHANGELOG +.. !! CAUTION !! + This file is automatically generated. Do not add new changelog + items when preparing a pull request. + The Documentation Changelog =========================== @@ -13,6 +17,997 @@ documentation. Do you also want to participate in the Symfony Documentation? Take a look at the ":doc:`/contributing/documentation/overview`" article. +October, 2016 +------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#7023 `_ Added useful debug commands in the debug documentation (hiddewie) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#7049 `_ Fix the platform.sh builds (wouterj) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#7090 `_ Remove suggestion to change the `.class` parameters (mpdude) +* `#7095 `_ Also mention "hasXXX" methods as validation targets (mpdude) +* `#7091 `_ A bracket was missed (hpatoio) +* `#7097 `_ link to specific HTTP Cache RFC (snoek09) +* `#7098 `_ Improved Redirecting paragraph of Testing page (ShinDarth) +* `#7020 `_ Added jQuery symfony-collection plugin to Form collection docs (hiddewie) +* `#7086 `_ Update bug documentation input console /console/input.rst (Quiss) +* `#7085 `_ Update custom_authentication_provider.rst (BatsaxIV) +* `#7083 `_ update github example (hootlex) +* `#7082 `_ Update metadata.rst (fberthereau) +* `#7061 `_ Change link to docs instead repo (mik-laj) +* `#7066 `_ Remove erroneous placeholder text (regularjack) +* `#7068 `_ Remove double spaces in some YAML configuration (michaelperrin) +* `#6785 `_ Twig reference: Add links from routing functions to special routing parameters (alexislefebvre) +* `#7043 `_ [Serializer] Move the see also block in the Learn More section (dunglas) +* `#7035 `_ Redirect /form to /forms for consistency (wouterj) +* `#7054 `_ Fix IS_AUTHENTICATED_FULLY annotation (mschobner) +* `#7044 `_ Add Nginx configuration to environment variables (peterkokot) +* `#7053 `_ Minor improvements for the contribution guide (javiereguiluz) +* `#7050 `_ use single quotes for YAML strings (snoek09) +* `#7047 `_ Fix typo in doctrine.rst (to manage) (lacyrhoades) +* `#7046 `_ Fix incorrect callback validation example (mvar) +* `#7034 `_ Changed RFC links from drafts to proposed standarts (a-ast) +* `#7038 `_ Remove a dead link to the old PR header (dunglas) +* `#7037 `_ Fix a typo in the serializer doc (dunglas) +* `#7036 `_ Fix 404 error link for American English Oxford Dictionary (peterkokot) +* `#6980 `_ Use strict comparison (greg0ire) +* `#7025 `_ Update buisness-logic (zairigimad) +* `#7027 `_ Remove dash prefix from route name (bocharsky-bw) +* `#7028 `_ A few minor tweaks (bocharsky-bw) +* `#7029 `_ refer to Symfony instead of Symfony2 (snoek09) +* `#7031 `_ Capitalize the time designator (simoheinonen) +* `#7018 `_ Reorder arguments: $request as the first argument (bocharsky-bw) +* `#7014 `_ Add a note about Filesystem:mkdir behavior (mickaelandrieu) +* `#6886 `_ Update controllers.rst (asandjivy) + +September, 2016 +--------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6976 `_ [Finishing][Serializer] Document the encoders (Ener-Getick, weaverryan) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#7016 `_ Add empty parentheses to method names (bocharsky-bw) +* `#7015 `_ Replace title placeholder name with slug (bocharsky-bw) +* `#7009 `_ Update form_dependencies.rst: fix DI (ReDnAxE) +* `#7010 `_ Update service_decoration.rst (lamari) +* `#6979 `_ Add specific tip about the http-kernel component (greg0ire) +* `#6686 `_ Update installation.rst (mgkimsal) +* `#7005 `_ Use new array syntax and make a few minor tweaks (bocharsky-bw) +* `#7004 `_ Tweak URL - CMF project moved to the other repo (bocharsky-bw) +* `#6999 `_ Several typo fixes (emirb) +* `#6997 `_ Update console.rst (adyassine) +* `#6917 `_ [Finder] document array use for locations (mickaelandrieu) +* `#6993 `_ Update create_custom_field_type.rst (yceruto) +* `#6995 `_ the least -> least (konrados) +* `#6934 `_ Update events.rst (asandjivy) +* `#6920 `_ [Config] Note about bundle priority for PrependExtensionInterface (wodor) +* `#6905 `_ Change example of ignoring dependencies for yaml (Integrity-178B) +* `#6885 `_ [FormComponent]Fix wrong mention in side note (rendler-denis) +* `#6911 `_ Article about logout. (BorodinDemid) +* `#6923 `_ Clarify by_reference use (jxmallett) +* `#6942 `_ Updated the "Build a Login Form" article (javiereguiluz) +* `#6933 `_ Misplaced paragraph about placeholders in routing.rst (antoin-m) +* `#6930 `_ Use Terminal lexer for console examples (wouterj) +* `#6893 `_ Update entity_provider.rst (asandjivy) +* `#6895 `_ fixing $formatLevelMap array values (zrashwani) +* `#6970 `_ Fix subject/verb agreement (micheal) +* `#6971 `_ Update composer.rst (TravisCarden) +* `#6983 `_ Update voters.rst (seferov) +* `#6986 `_ Fixed directory name typo (JoeThielen) +* `#6988 `_ fix link role syntax (xabbuh) +* `#6974 `_ Fix minor typo in security chapter How to Build a Traditional Login Form (peterkokot) +* `#6941 `_ Mentioned the "Symfony Upgrade Fixer" in the upgrade article (javiereguiluz) +* `#6936 `_ Improved the title of Validation Groups article to make it easier to find (javiereguiluz) +* `#6964 `_ Fix typo in validator example (svenluijten) +* `#6945 `_ Fixed indentation issues in alias_private article (javiereguiluz) +* `#6955 `_ Typo in the class name. (pythagor) + +August, 2016 +------------ + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6765 `_ [Contributing] [Standards] Do not use spaces inside/around offset accessors (phansys) +* `#6746 `_ Removing the alias stuff - not required after symfony/symfony#17074 (weaverryan) +* `#6798 `_ Finishing Validator Docs (wouterj, mickaelandrieu, javiereguiluz, weaverryan) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6915 `_ Fix the error in code example (kruglikov) +* `#6907 `_ fix wrong variable name in OptionsResolver example (dincho) +* `#6904 `_ Update create_custom_field_type.rst (tuanalumi) +* `#6884 `_ service_container : fix php Definition instance (ReDnAxE) +* `#6883 `_ [Routing] Fix a route path in a routing example (thomasbisignani) +* `#6869 `_ Update templating.rst (asandjivy) +* `#6822 `_ Adjust Application use statement (kvdnberg) +* `#6881 `_ Error in CSRF example code snippet (makoru-hikage) +* `#6848 `_ Fix Varnish 4 code example (Dreimus) +* `#6845 `_ Fixed the extension of a logging article (javiereguiluz) +* `#6800 `_ Fix missing function name (JosefVitu) +* `#6843 `_ fix index directive syntax (xabbuh) +* `#6784 `_ Fix CS for form templates locations (javiereguiluz) +* `#6797 `_ fix FlattenException namespace (alchimik) +* `#6787 `_ Fix reference to output object (micheal) +* `#6757 `_ Fix typo in external_parameters.rst (gmorel) +* `#6754 `_ Add missing use statements to data collector example (richardmiller) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6921 `_ Fix var_dumper advanced usage link (ogizanagi) +* `#6913 `_ [Controller Description] Fix typos and class link (rendler-denis) +* `#6909 `_ DumpFile() third argument is deprecated and doesn't exists anymore (mickaelandrieu) +* `#6901 `_ [EventDispatcher] paragraph duplicated (ReDnAxE) +* `#6899 `_ Update access_control.rst (asandjivy) +* `#6898 `_ Fixes after tonight's merge round (wouterj) +* `#6849 `_ Link to inversedBy/mappedBy documentation (soulchainer, wouterj) +* `#6846 `_ Adds bin folder creation instruction (joelrfcosta) +* `#6835 `_ Updated the instructions to build docs locally (javiereguiluz) +* 5a5720fe minor #6834 Refactored how to get the container in a test (javiereguiluz) +* `#6814 `_ Created the main article about "deployment" (javiereguiluz) +* `#6882 `_ Update serializer.rst (seferov) +* `#6880 `_ Remove extra quotes from ExprBuilder::thenInvalid() usage (chalasr) +* `#6878 `_ missing "`" (jevgenijusr) +* `#6877 `_ added lyrixx to the core team (fabpot) +* `#6867 `_ Add a class specificity for SplFileInfo text (rendler-denis) +* `#6872 `_ Remove redundant verb (julienfalque) +* `#6866 `_ Deprecated message with "true" parameter (wazz42) +* `#6862 `_ Remove unused JsonResponse dependency in example (Farskies) +* `#6855 `_ [Form] Use composer require instead of modifying composer.json (wouterj) +* `#6853 `_ Logrotate moved to GitHub (wouterj) +* `#6851 `_ Update lazy_services.rst (takeit) +* `#6794 `_ Added a new section to the page templating/global_vars using a EVListener (piet, Piet Bijl) +* `#6824 `_ Service naming convension (orions) +* `#6829 `_ Fix a typo in an HTTP Cache code example (aybbou) +* `#6833 `_ Fixed a syntax issue in custom_constraint article (javiereguiluz) +* `#6842 `_ Fixed service name (jeremyFreeAgent) +* `#6803 `_ Remove complex speech pattern (micheal) +* `#6805 `_ Remove colloquialism "hold on" (micheal) +* `#6796 `_ Remove AcmeDemoBundle references (michaelcullum) +* `#6786 `_ Subject-verb agreement (micheal) +* `#6759 `_ Fix tense and sentence length (aalaap) +* `#6820 `_ Fixed the main index page redirections (javiereguiluz) +* `#6819 `_ Fixed the redirection for "upgrade" articles (javiereguiluz) +* `#6812 `_ Fixed a Console article redirection (javiereguiluz) +* `#6807 `_ Fixed the redirection of the cookbook/psr7 article (javiereguiluz) +* `#6806 `_ Fixed the redirection of some cache articles (javiereguiluz) +* `#6808 `_ Fixed a DI redirection (javiereguiluz) +* `#6810 `_ Fixed the redirection of the previous "performance" book chapter (javiereguiluz) +* `#6816 `_ Added all the missing "index pages" redirections (javiereguiluz) + +July, 2016 +---------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6611 `_ Discourage the use of controllers as services (javiereguiluz) +* `#5672 `_ Add constants to BC promise (WouterJ) +* `#6707 `_ Describe serialization config location in cookbook (jcrombez, WouterJ) +* `#6726 `_ Use getParameter method in controllers (peterkokot) +* `#6701 `_ [CS] Avoid using useless expressions (phansys) +* `#6673 `_ Caution about impersonation not compatible with pre authenticated (pasdeloup) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6634 `_ Update custom_constraint.rst (axelvnk) +* `#6719 `_ [Components][Browser-Kit]Fix typo with CookieJar (Denis-Florin Rendler) +* `#6687 `_ Namespace fix (JhonnyL) +* `#6704 `_ Encountered an error when following the steps for contribution (chancegarcia) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6778 `_ fix syntax errors (xabbuh) +* `#6777 `_ fix code block indentation (xabbuh) +* `#108 `_ fix another bug due to choosing the wrong branch (xabbuh) +* `#105 `_ fix bugs due to choosing the wrong base branch (xabbuh) +* `#102 `_ Updated the Global Composer Installation article (javiereguiluz) +* `#101 `_ Update some screenshots to wrap them with a browser window (javiereguiluz) +* `#104 `_ complete component cross references (xabbuh) +* `#106 `_ some minor tweaks (xabbuh) +* `#98 `_ Remove mentions of cookbook/book (WouterJ) +* `#97 `_ Rewrote the Console articles (WouterJ, javiereguiluz) +* `#99 `_ Rename cache/ to http_cache/ (WouterJ) +* `#100 `_ Add file extension to SOAP article (WouterJ) +* `#92 `_ Make usage of "The" in the edition list consistent (WouterJ) +* `#91 `_ Create a section for "Getting Started" so we can generate a book (javiereguiluz) +* `#77 `_ Proofing the controller chapter (weaverryan) +* `#90 `_ Fixed doc build issues (javiereguiluz) +* `#79 `_ Shortening the setup section (weaverryan) +* `#81 `_ Merging setup and install directories (weaverryan) +* `#84 `_ Bootstrapping the validator components (weaverryan) +* `#87 `_ Moving the email guide to the top level (weaverryan) +* `#88 `_ Moving event_dispatcher/event_listener.rst -> event_dispatcher.rst (weaverryan) +* `#78 `_ Move redirection_map from root (WouterJ) +* `#54 `_ split the Security chapter (xabbuh) +* `#53 `_ split the Validation chapter (xabbuh) +* `#63 `_ Readded removed versionadded directives (WouterJ) +* `#55 `_ Created the "Set Up" topic (WouterJ) +* `#62 `_ Rename includes directory to _includes (WouterJ) +* `#61 `_ Fix install/upgrade references (WouterJ) +* `#58 `_ The no-brainer topic merges/removal (WouterJ) +* `#56 `_ Fix build errors (WouterJ) +* `#39 `_ Deleting index files - using globbing (weaverryan, WouterJ) +* `#47 `_ Move nested service container articles to sub-topic root (WouterJ) +* `#50 `_ Move images to _images and group by topic (WouterJ) +* `#32 `_ Move all cookbook contents (javiereguiluz) +* `#28 `_ split the routing chapter (xabbuh) +* `#30 `_ Moved the rest of the book chapters (javiereguiluz) +* `#24 `_ Moved book chapters out of the book (javiereguiluz) +* `#20 `_ Creating the Controller topic (xabbuh) +* `#6747 `_ Correcting reference to ``isSuccessful()`` method for Process (aedmonds) +* `#6600 `_ Removing some extra details from #6444 (weaverryan) +* `#6715 `_ [Book] Remove DI extension info and link the cookbook article instead (WouterJ) +* `#6745 `_ Branch fix (Talita Kocjan Zager, weaverryan) +* `#6656 `_ Clarify usage of handler channel configuration (shkkmo) +* `#6664 `_ replace occurrences of `_ Add little comment indicating meaning of $firewall variable (ruslan-fidesio, WouterJ) +* `#6734 `_ Add little caution to add service id for @Route annotation (DHager, WouterJ) +* `#6735 `_ Change _method parameter versionadded note (sfdumi, WouterJ) +* `#6736 `_ Use message argument for PHPunit assert() functions (SimonHeimberg, WouterJ) +* `#6739 `_ fix list item termination character (xabbuh) +* `#6218 `_ path() explanation inside templating + Minor formatting changes (sfdumi) +* `#6559 `_ Update lazy_services.rst (hboomsma) +* `#6733 `_ [DX] Form Types location contradicts Best Practices (pbowyer) +* `#6264 `_ Update email.rst (mikaelz) +* `#6633 `_ Added escaping tip (xDaizu) +* `#5464 `_ Removed the glossary (WouterJ) +* `#6665 `_ use PDO prepared statement - avoid straw man (dr-matt-smith) +* `#6700 `_ Update monolog.rst (zhil) +* `#6720 `_ [Component][ClassLoader]Remove invalid note (rendler-denis) +* `#6613 `_ Clarify documentation on serving files (raphaelm) +* `#6721 `_ [Finder] Fixed typo in RealPath method on SplFileInfo class (acrobat) +* `#6716 `_ Typo fix "they the name" => "that the name" (jevgenijusr) +* `#6709 `_ Fix URL in http basic screenshot (WouterJ) +* `#6706 `_ Update "How to Authenticate Users with API Keys" (gondo, WouterJ) +* `#5892 `_ Updated the session proxy article (javiereguiluz) +* `#6697 `_ [Asset] add versionadded directive (xabbuh) + +June, 2016 +---------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6587 `_ Updating recommended email settings for monolog (jorgelbg) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6679 `_ Invalid PHP return statement (JohnnyEvo) +* `#6675 `_ Update broken links to default VCL files (sgrodzicki) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6597 `_ [Validator] Add shorter examples using the default option (alexmart) +* `#6696 `_ Typo fix (jevgenijusr) +* `#6614 `_ Updated the CS rule about return null; and return; (javiereguiluz) +* `#6692 `_ Update date.rst - Fixes typo (fdarre) +* `#6689 `_ Hard values for the driver option (iltar) +* `#6685 `_ NullOutput should be passed to $command->run() (Ma27) +* `#6676 `_ Removed 'html' from the component description (naroga) +* `#6674 `_ CheckStyle in Voters cookbook (JakeFr) +* `#6672 `_ [Book][Testing] remove Symfony core testing note (xabbuh) +* `#6670 `_ Fix typo 'even' >> 'event' in event_listener.rst (kuusas) +* `#6667 `_ [Contributing][Code] fix list item terminators (xabbuh) +* `#6616 `_ Better explain the mandatory/convention location of some elements (rcousens, javiereguiluz) +* `#6628 `_ Fix for #6625 (kix) +* `#6668 `_ [Contributing][Code] remove PHPUnit requirement (xabbuh) +* `#6654 `_ Update upload_file.rst (liubinas) +* `#6650 `_ fix dumper default representation (Jamal Youssefi) +* `#6652 `_ ``Finder::path()`` method matching directories and files (soyuka) +* `#6662 `_ preg_match throw an warning (nicolae-stelian) +* `#6658 `_ [Process] tweak a sentence (xabbuh) +* `#6638 `_ swap terminate and exception event descriptions (xabbuh) +* `#6615 `_ Minor grammar fix (aalaap) +* `#6637 `_ Update security.rst (norbert-n) +* `#6644 `_ [Console] Fix wrong quotes in QuestionHelper::setInputStream() (chalasr) +* `#6645 `_ Fix bootstrap class name help-block (foaly-nr1) +* `#6641 `_ [Book][Form] fix reference to renamed document (xabbuh) +* `#6579 `_ Added callable validation_groups example (gnat42) +* `#6626 `_ reflect the EOM of Symfony 2.3 (xabbuh) +* `#6631 `_ Fix console.exception and console.terminate order (Julien Falque) +* `#6629 `_ Update options_resolver.rst (atailouloute) +* `#6627 `_ Fixed a typo in cookbook/security/entity_provider (michaeldegroot) +* `#6618 `_ Added a note about coding standards and method arguments (javiereguiluz) + +May, 2016 +--------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6040 `_ Remove old File Upload article + improve the new one (WouterJ) +* `#6412 `_ added a maintenance document (fabpot) +* `#6554 `_ Adding information about using the date type as usable date picker field (weaverryan) +* `#6590 `_ Added note on YAML mappings as objects (dantleech) +* `#6583 `_ Adding a description for the use_microseconds parameter introduced in MonologBundle v2.11 (jorgelbg) +* `#6582 `_ Advanced YAML component usage (dantleech) +* `#6405 `_ Added the explanation about addClassesToCompile() method (javiereguiluz) +* `#6539 `_ Documented the "autoescape" TwigBundle config option (javiereguiluz) +* `#5574 `_ [2.7] Update Twig docs for asset features (javiereguiluz, WouterJ) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6619 `_ Fix wrong variable name in comment (zanardigit) +* `#6606 `_ fix #6602 (yamiko-ninja) +* `#6578 `_ [Cookbook][Profiler] Fix arguments for Profiler::find() (hason) +* `#6564 `_ [PhpUnitBridge] Remove section about clock mocking (z38) +* `#6552 `_ Typo fix in the Serializer deserialization example for existing object (fre5h) +* `#6545 `_ Replace property_accessor by property_access (jbenoit2011) +* `#6561 `_ About Templating Naming Pattern (raulconti) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6620 `_ Move PSR to correct place on index page (WouterJ) +* `#6609 `_ Use a better escaping mechanism for data-prototype attr (javiereguiluz) +* `#6380 `_ [book] [validation] Constraints can be applied to an entire class too (sustmi) +* `#6444 `_ [Form] fixed EntityType choice options (HeahDude) +* `#6367 `_ Simplified the contribution article for Symfony Docs (javiereguiluz) +* `#6419 `_ Update routing.rst (tamtamchik) +* `#6598 `_ [Yaml] use static Yaml API (xabbuh) +* `#6589 `_ Clarify signed requests in the ESI renderer (WouterJ) +* `#6596 `_ Fixed query_builder option (HeahDude) +* `#6595 `_ Added a note about "encoding vs. hashing" passwords (javiereguiluz) +* `#6581 `_ [#6431] changing "Simple Example" to use implode/explode (mccullagh) +* `#6585 `_ 6555 link to Download instructions page & Windows executable (snoek09) +* `#6588 `_ Update configuration.rst (superhaggis) +* `#6591 `_ 5953 use kernel events constants (snoek09) +* `#6592 `_ [Form] Making the name property private to be more realistic (weaverryan) +* `#6541 `_ Trusted proxies were removed when URL signing took over (rawkode) +* `#6586 `_ 6338 use csrfManager instead of csrfProvider (snoek09) +* `#6401 `_ Added Link to Cmder (c33s) +* `#6391 `_ Fix mem leak in example doctrine testing (nicolas-grekas) +* `#6087 `_ Add a note about needing to install proxy-manager (mcfedr) +* `#6553 `_ EntityType: query_builder link to usage (weaverryan) +* `#6572 `_ Edited BowerPHP tip (SecondeJK) +* `#6575 `_ Rename command logging services (sroze) +* `#6571 `_ [Cookbook][Console] Minor: Fix typo (andreia) +* `#6568 `_ fix RequestDataCollector class namespace (xabbuh) +* `#6566 `_ Update options_resolver.rst (snake77se) +* `#6562 `_ Remove extra spaces in Nginx template (bocharsky-bw) +* `#6557 `_ [ClassLoader] Add missed link to the external PSR-4 specification (nicolas-grekas, fre5h) +* `#6511 `_ [DependencyInjection] Improved "optional argument" documentation (dantleech) +* `#6455 `_ Editing the Doctrine section to improve accuracy and readability (natechicago) +* `#6526 `_ Documented how to configure Symfony correctly with regards to the Forwarded header (magnusnordlander) +* `#6535 `_ Improved the description of the Twig global variables (javiereguiluz) +* `#6530 `_ [DependencyInjection] Unquote services FQCN in parent-services examples (chalasr) +* `#6517 `_ Add a warning about using same user for cli and web server (pasdeloup) +* `#6504 `_ Improved the docs for the DependencyInjection component (javiereguiluz) +* `#6506 `_ Added a tip about routes and container parameters (javiereguiluz) +* `#6518 `_ Add details about chmod +a vs setfacl (pasdeloup) +* `#6525 `_ [Contributing] use more precise version checker URL (xabbuh) +* `#6528 `_ Fixed a minor indentation issue (javiereguiluz) + +April, 2016 +----------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6470 `_ Documented the config options of TwigBundle (javiereguiluz) +* `#6427 `_ [Testing] Explain how to add or remove data in a collection of forms (alexislefebvre) +* `#6394 `_ Updated Heroku instructions (magnusnordlander) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6503 `_ Fix typo in from flat PHP to Symfony (gregfriedrice, WouterJ) +* `#6483 `_ Fix typo: signifcantly => significantly (ifdattic) +* `#6482 `_ fixed wrong secret string in array examples (OskarStark) +* `#6460 `_ Update authorization.rst (mantulo) +* `#6451 `_ fix status code in snippet (Barno) +* `#6448 `_ [Form] fixed CollectionType needless option (HeahDude) +* `#6439 `_ Fix form/validation directory path (nemo-) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6522 `_ On line 360 the ``404 Not Found`` header not working (mbrig-co) +* `#6521 `_ The ``$link`` argument must be passed as a reference (mbrig-co) +* `#6523 `_ Fix snippet (ismailbaskin) +* `#6472 `_ Avoid confusion (gerryvdm) +* `#6300 `_ Document constraint validator alias optional (Triiistan) +* `#6513 `_ remove documentation of not supported "verbose" option value (TobiasXy) +* `#6510 `_ use port 587 in Amazon SES example (snoek09) +* `#6464 `_ Added possible values for access_decision_manager.strategy (AAstakhov) +* `#6478 `_ Replace reference to the request service (gerryvdm) +* `#6479 `_ Update php.rst (carlos-granados) +* `#6481 `_ Remove reference to Symfony2 in request-flow.png (Daniel Cotton) +* `#6449 `_ [Form] fixed ChoiceType example in CollectionType (HeahDude) +* `#6445 `_ updated the core team (fabpot) +* `#6423 `_ Added a caution note about REMOTE_USER and user impersonation (javiereguiluz) +* `#6452 `_ Use different placeholders in mailer config (sblaut) +* `#6457 `_ Fixed an array notation in comment (serializer.rst) (iltar) +* `#6456 `_ Fixed array [] notation and trailing spaces (iltar) +* `#6420 `_ Added tip for optional second parameter for form submissions. (Michael Phillips) +* `#6418 `_ fix spelling of the flashBag() method (xabbuh) + +March, 2016 +----------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6282 `_ [Form] fix ``choice_label`` values (HeahDude) +* `#5894 `_ [WIP] Added an article to explain how to upgrade third-party bundles to Symfony 3 (javiereguiluz) +* `#6273 `_ [PHPUnit bridge] Add documentation for the component (theofidry) +* `#6291 `_ fortrabbit deployment guide + index listing (ostark) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6366 `_ Removed server:stop code block for 2.3 (theyoux) +* `#6347 `_ Add a note about enabling DebugBundle to use VarDumper inside Symfony (martijn80, javiereguiluz) +* `#6320 `_ Fixed typo in path (timhovius) +* `#6334 `_ Fixed yaml configuration of app.exception_controller (AAstakhov) +* `#6315 `_ Remove third parameter from createFormBuilder call (Hocdoc) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6404 `_ fixed a typo (RickieL) +* `#6411 `_ Fixed a typo in configuration-block (VarunAgw) +* `#6414 `_ stick to Sphinx 1.3.x for the moment (xabbuh) +* `#6399 `_ Fixed wrong code examples for Isbn constraint (AAstakhov) +* `#6397 `_ Fix typo in SwitchUserListener file name (luxifer) +* `#6382 `_ unused use instructions (bshevchenko) +* `#6365 `_ Removed the PR table example (this is now included by GitHub template) (javiereguiluz) +* `#6363 `_ Removed info about reducing visibility for private (AAstakhov) +* `#6362 `_ [book] Updated link to Translatable Extension (AAstakhov) +* `#6336 `_ Added minor clarification (ThomasLandauer) +* `#6303 `_ Tweaked the Symfony Releases page (javiereguiluz) +* `#6360 `_ Editing the Doctrine section to improve accuracy and readability (natechicago) +* `#6352 `_ [book] controller ch review, part 3 (Talita Kocjan Zager) +* `#6351 `_ [book] controller ch review, part 2 (Talita Kocjan Zager) +* `#6349 `_ [book] controller ch review, part 1 (Talita Kocjan Zager) +* `#6369 `_ Minor corrections (sfdumi) +* `#6370 `_ Fixed typo (tabbi89) +* `#6371 `_ Fix escaping of backtick inside double back-quotes (guilliamxavier) +* `#6364 `_ [reference] [constraints] added missing colon character for Image constraint documentation in YAML format. (hhamon) +* `#6345 `_ Remove link-local IPv6 address (snoek09) +* `#6219 `_ Point that route parameters are also Request attributes (sfdumi) +* `#6348 `_ [best practices] mostly typos (Talita Kocjan Zager) +* `#6275 `_ [quick tour] mostly typos (Talita Kocjan Zager) +* `#6305 `_ Mention IvoryCKEditorBundle in the Symfony Forms doc (javiereguiluz) +* `#6331 `_ Rename DunglasApiBundle to ApiPlatform (sroze) +* `#6328 `_ Update extension.rst - added caution box for people trying to remove the default file with services definitions (Pavel Jurecka) +* `#6343 `_ Replace XLIFF number ids by strings (Triiistan) +* `#6344 `_ Altered single / multiple inheritance sentence (outspaced) +* `#6330 `_ [Form] reorder EntityType options (HeahDude) +* `#6337 `_ Fix configuration.rst typo (gong023) +* `#6295 `_ Update tools.rst (andrewtch) +* `#6325 `_ Minor error (ThomasLandauer) +* `#6311 `_ Improved TwigExtension to show default values and optional arguments (javiereguiluz) +* `#6267 `_ [Form] fix 'data_class' option in EntityType (HeahDude) +* `#6281 `_ Change isValid to isSubmitted. (mustafaaloko) + +February, 2016 +-------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6172 `_ move assets options from templating to assets section and add base_path documentation (snoek09) +* `#6021 `_ mention routing from the database (dbu) +* `#6233 `_ Document translation_domain for choice fields (merorafael, WouterJ) +* `#5655 `_ Added doc about Homestead's Symfony integration (WouterJ) +* `#6072 `_ Add browserkit component documentation (yamiko, yamiko-ninja, robert Parker, javiereguiluz) +* `#6243 `_ Add missing getBoolean() method (bocharsky-bw) +* `#6231 `_ Use hash_equals instead of StringUtils::equals (WouterJ) +* `#5724 `_ Describe configuration behaviour with multiple mailers (xelan) +* `#6077 `_ fixes #5971 (vincentaubert) +* `#6156 `_ [reference] [form] [options] fix #6153 (HeahDude) +* `#6104 `_ Fix #6103 (zsturgess) +* `#5856 `_ Reworded the "How to use Gmail" cookbook article (javiereguiluz) +* `#6230 `_ Add annotation to glossary (rebased) (DerStoffel) +* `#5642 `_ Documented label_format option (WouterJ) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#5995 `_ Update dev_environment.rst (gonzalovilaseca) +* `#6240 `_ [#6224] some tweaks (xabbuh) +* `#5513 `_ [load_balancer_reverse_proxy ] Always use 127.0.0.1 as a trusted proxy (ruudk) +* `#6124 `_ [cookbook] Add annotations block and fix regex (peterkokot) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6308 `_ fix literal syntax (xabbuh) +* `#6251 `_ To use annotations, files must be removed (pbowyer) +* `#6288 `_ Update factories.rst (velikanov) +* `#6278 `_ [HttpFoundation] Fix typo for ParameterBag getters (rendler-denis) +* `#6280 `_ Fix syntax of Company class example (cakper) +* `#6284 `_ [Book] [Routing] Fix third param true to UrlGeneratorInterface::ABSOLUTE_URI (eriwin) +* `#6269 `_ [Cookbook][Bundles]fix yaml syntax (mhor) +* `#6255 `_ [Cookbook][Doctrine] some tweaks to the Doctrine registration article (xabbuh) +* `#6229 `_ Rewrite EventDispatcher introduction (WouterJ) +* `#6260 `_ add missing options `choice_value`, `choice_name` and `choice_attr` to `EntityType` (HeahDude) +* `#6262 `_ [Form] reorder options in choice types references (HeahDude) +* `#6256 `_ Fixed code example (twifty) +* `#6250 `_ [Cookbook][Console] remove note about memory spool handling on CLI (xabbuh) +* `#6249 `_ [Cookbook][Serializer] fix wording (xabbuh) +* `#6246 `_ removed duplicate lines (seferov) +* `#6222 `_ Updated "Learn more from the Cookbook" section (sfdumi) +* `#6245 `_ [Cookbook][Console] change API doc class name (xabbuh) +* `#6223 `_ Improveme the apache/mod_php configuration example (gnat42) +* `#6234 `_ File System Security Issue in Custom Auth Article (finished) (mattjanssen, WouterJ) +* `#4773 `_ [Cookbook] Make registration_form follow best practices (xelaris) +* `#5630 `_ Add a caution about logout when using http-basic authenticated firewall (rmed19) +* `#6215 `_ Added a caution about failing cache warmers (javiereguiluz) +* `#6239 `_ Remove app_dev as build-in server is used (rmed19, WouterJ) +* `#6241 `_ [ExpressionLanguage] Add caution about backslash handling (zerustech, WouterJ) +* `#6236 `_ fix some minor typos (xabbuh) +* `#6237 `_ use literals for external class names (xabbuh) +* `#6206 `_ add separate placeholder examples for birthday, datetime and time type (snoek09) +* `#6238 `_ fix directive name (xabbuh) +* `#6224 `_ Note to create a service if you extend ExceptionController (pamuche) +* `#5958 `_ Update security.rst (mpaquet) +* `#6092 `_ Updated information about testing code coverage. (roga) +* `#6051 `_ Mention HautelookAliceBundle in best practices (theofidry, WouterJ) +* `#6220 `_ [book] fixes typo about redirect status codes in the controller chapter. (hhamon) +* `#6227 `_ Update testing.rst (dvapelnik) +* `#6212 `_ Typo in default session save_path (DerekRoth) +* `#6208 `_ Replace references of PSR-0 with PSR-4 (opdavies) +* `#6190 `_ Fix redundant command line sample (sylozof) + +January, 2016 +------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#6174 `_ Missing reference docs for kernel.finish_request event (acrobat) +* `#6184 `_ added Javier as a merger for the WebProfiler bundle (fabpot) +* `#5303 `_ [WIP] 4373 - document security events (kevintweber) +* `#6023 `_ clarify the routing component documentation a bit (dbu) +* `#6091 `_ Added an example for info() method (javiereguiluz) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6193 `_ Added the missing namespace in example of a subscriber class (raulconti) +* `#6152 `_ csrf_token_generator and csrf_token_id documentation (Raistlfiren, Aaron Valandra, xabbuh) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6207 `_ revert form login CSRF changes on wrong branch (xabbuh) +* `#6191 `_ Document the invalidate_session option (javiereguiluz) +* `#6141 `_ Docs do not match functionality (Loupax) +* `#6192 `_ fix MongoDB shell syntax highlighting (xabbuh) +* `#6147 `_ Update templating.rst - Asset absolute url fix (gbalcewicz) +* `#6187 `_ Typofix for "Defining and Processing Configuration Values" (kix) +* `#6183 `_ use valid XML in code block (xabbuh) +* `#6180 `_ use single quotes for YAML strings (snoek09) +* `#6070 `_ Typo in When Things Get More Advanced (BallisticPain) +* `#6119 `_ Remove phrase "in order" (micheal) +* `#6160 `_ remove versionadded for unmaintained versions (xabbuh) +* `#6161 `_ [Contributing][Documentation] replace EOL with EOM (xabbuh) +* `#6162 `_ [Reference] add missing version number (xabbuh) +* `#6163 `_ Remove excessive pluses (aivus) +* `#6158 `_ Update override_dir_structure.rst (denniskoenigComparon) +* `#6122 `_ Added missing mandatory parameter (yceruto) +* `#6100 `_ [Cookbook][Security] add back updateUserSecurityIdentity() hint (xabbuh) +* `#6138 `_ Correction needed (pfleu) +* `#6133 `_ fixed the component name (fabpot) +* `#6127 `_ escape namespace backslashes in class role (xabbuh) +* `#5818 `_ document old way of checking validity of CSRF token (snoek09) +* `#6062 `_ Update HTTP method requirement example (WouterJ) +* `#6109 `_ add link to Monolog configuration (snoek09) +* `#6096 `_ [Contributing] update year in license (xabbuh) +* `#6114 `_ make method protected (OskarStark) +* `#6111 `_ Fixed a typo in the choice_label code example (ferdynator) +* `#6102 `_ promoted xabbuh as merger on the Yaml component (fabpot) +* `#6013 `_ [2.7][Form] placeholder option: replace "in favor" misuses (ogizanagi) + +December, 2015 +-------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#5906 `_ Added documentation for choice_translation_domain option (peterrehm) +* `#6017 `_ Documented the Symfony Console Styles (javiereguiluz) +* `#5811 `_ Conversion from mysql to PDO (iqbalmalik89) +* `#5962 `_ Simplify code example in "Adding custom extensions" section (snoek09) +* `#6022 `_ clarify custom route loader documentation (dbu) +* `#5994 `_ Updated the release process for Symfony 3.x and future releases (javiereguiluz) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#6063 `_ minor #5829 Fix broken composer command (JHGitty) +* `#5904 `_ Update php_soap_extension.rst (xDaizu) +* `#5819 `_ Remove AppBundle (roukmoute) +* `#6001 `_ Fix class name (BlueM) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#6043 `_ Mention commiting only bower.json (krike, WouterJ) +* `#5848 `_ Added hints to spool config section (martinczerwi) +* `#6042 `_ some tweaks to unit testing form types (xabbuh) +* `#6059 `_ Add best practice about the Form type namespace (WouterJ) +* `#6068 `_ Remove references to API tagging (dunglas) +* `#6088 `_ Update validation.rst (syedi) +* `#6085 `_ Update validation.rst (syedi) +* `#6094 `_ [Form] Added a missing php opening tag (dev-symfony-void) +* `#5840 `_ [Contributing] [Standards] Add note about ``trigger_error()`` and deprecation messages (phansys) +* `#6050 `_ Lots of minor fixes & applying best practices to form cookbook doc (ThomasLandauer, WouterJ) +* `#5570 `_ Quick review of 'create framework' tutorial (WouterJ) +* `#5445 `_ Reworded the explanation about the kernel.event_listener tag (javiereguiluz) +* `#6054 `_ Remove 2.8 branch from patch documentation (Triiistan) +* `#6057 `_ Fix PHP code for registering service (WouterJ) +* `#6067 `_ improve phrasing (greg0ire) +* `#6063 `_ minor #5829 Fix broken composer command (JHGitty) +* `#6041 `_ Fixed misspelling of human in glossary.rst YAML (Wasserschlange) +* `#6049 `_ Finish #5798 Add ``app_`` prefix to form type names (OskarStark, WouterJ) +* `#5829 `_ use composer command instead of editing json file (OskarStark) +* `#6046 `_ Update framework.rst (typo in sesssion) (patrick-mota) +* `#5662 `_ Fixed wrong version of symfony with composer install (Nek-) +* `#5890 `_ Updated article for modern Symfony practices and the use of bcrypt (javiereguiluz) +* `#6015 `_ [Assetic] complete XML configuration examples (xabbuh) +* `#5963 `_ Add note about 'phar extension' dependency (snoek09) +* `#6006 `_ [Book] use AppBundle examples and follow best practices (xabbuh) +* `#6016 `_ Corrected the line references for the basic controller example (theTeddyBear) +* `#5446 `_ [Contributing] [Standards] Added note about phpdoc_separation (phansys) +* `#5820 `_ Fixed an issue with command option shortcuts (javiereguiluz) +* `#6033 `_ Fix Typo (Shine-neko) +* `#6011 `_ Fixed formatting issues (javiereguiluz) +* `#6012 `_ Use HTTPS for downloading the Symfony Installer (javiereguiluz) +* `#6009 `_ Fix missing constant usage for generating urls (Tobion) +* `#5965 `_ Removing php opening tags (Deamon) +* `#6003 `_ #5999 fix files names (vincentaubert) +* `#5996 `_ Clarify example for SUBMIT form event (bkosborne) +* `#6000 `_ Update registration_form.rst (afurculita) +* `#5989 `_ Fix words according context (richardpq) +* `#5992 `_ More use single quotes for YAML strings (snoek09) +* `#5957 `_ mark deep option as deprecated (snoek09) +* `#5943 `_ Add tip for when returning ``null`` from ``createToken()`` (jeroenseegers) +* `#5956 `_ Update security.rst (mpaquet) +* `#5959 `_ Fix #5912 Ambiguity on Access Decision Manager's Strategy (Pierre Maraitre) +* `#5955 `_ use single quotes for YAML strings (snoek09) +* `#5979 `_ [Book] Do not extend the base controller before introducing it (ogizanagi) +* `#5970 `_ Remove isSubmitted call (DanielSiepmann) +* `#5972 `_ Add isSubmitted call (DanielSiepmann) +* `#5961 `_ update from_flat_php_to_symfony2.rst (thao-witkam) + +November, 2015 +-------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#5893 `_ Added a note about the use of _format query parameter (javiereguiluz) +* `#5876 `_ Symfony 2.7 Form choice option update (aivus, althaus, weaverryan) +* `#5861 `_ Updated Table Console helper for spanning cols and rows (hiddewie) +* `#5816 `_ Merge branches (nicolas-grekas, snoek09, WouterJ, xabbuh) +* `#5804 `_ Added documentation for dnsMessage option (BenjaminPaap) +* `#5774 `_ Show a more real example in data collectors doc (WouterJ) +* `#5735 `_ [Contributing][Code] do not distinguish regular classes and API classes (xabbuh) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#5903 `_ Update front controller (nurolopher) +* `#5768 `_ Removed "http_basic" config from the login form cookbook (javiereguiluz) +* `#5863 `_ Correct useAttributeAsKey usage (danrot) +* `#5833 `_ Fixed whitelist delivery of swiftmailer (hiddewie) +* `#5815 `_ fix constraint names (xabbuh) +* `#5793 `_ Callback Validation Constraint: Remove reference to deprecated option (ceithir) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#5931 `_ [#5875] Fixed link description, list of common media types (Douglas Naphas) +* `#5923 `_ Remove information about request service deps of core services (WouterJ) +* `#5911 `_ Wrap all strings containing @ in quotes in Yaml (WouterJ) +* `#5889 `_ Always use "main" as the default firewall name (to match Symfony Standard Edition) (javiereguiluz) +* `#5888 `_ Removed the use of ContainerAware class (javiereguiluz) +* `#5625 `_ Tell about ``SYMFONY__TEMPLATING__HELPER__CODE__FILE_LINK_FORMAT`` (nicolas-grekas) +* `#5896 `_ [Book][Templating] Update absolute URL asset to match 2.7 (lemoinem) +* `#5828 `_ move the getEntityManager, only get it if needed (OskarStark) +* `#5900 `_ Added new security advisories to the docs (fabpot) +* `#5897 `_ Fixed some wrong line number references in doctrine.rst (DigNative) +* `#5895 `_ Update debug_formatter.rst (strannik-06) +* `#5883 `_ Book: Update Service Container Documentation (zanderbaldwin) +* `#5862 `_ Fixes done automatically by the docbot (WouterJ) +* `#5851 `_ updated sentence (OskarStark) +* `#5870 `_ Update securing_services.rst (aruku) +* `#5859 `_ Use Twig highlighter instead of Jinja (WouterJ) +* `#5866 `_ Fixed little typo with a twig example (artf) +* `#5849 `_ Clarified ambiguous wording (ThomasLandauer) +* `#5826 `_ "setup" is a noun or adjective, "set up" is the verb (carlos-granados) +* `#5816 `_ Merge branches (nicolas-grekas, snoek09, WouterJ, xabbuh) +* `#5813 `_ use constants to choose generated URL type (xabbuh) +* `#5808 `_ Reworded the explanation about flash messages (javiereguiluz) +* `#5809 `_ Minor fix (javiereguiluz) +* `#5805 `_ Mentioned the BETA and RC support for the Symfony Installer (javiereguiluz) +* `#5781 `_ Added annotations example to Linking to Pages examples (carlos-granados) +* `#5780 `_ Clarify when we are talking about PHP and Twig (carlos-granados) +* `#5767 `_ [Cookbook][Security] clarify description of the getPosition() method (xabbuh) +* `#5731 `_ [Cookbook][Security] update versionadded directive to match the content (xabbuh) +* `#5681 `_ Update storage.rst (jls2933) +* `#5363 `_ Added description on how to enable the security:check command through… (bizmate) +* `#5841 `_ [Cookbook][Psr7] fix zend-diactoros Packagist link (xabbuh) +* `#5850 `_ Fixed typo (tobiassjosten) +* `#5852 `_ Fix doc for 2.6+, `server:start` replace `...:run` (Kevinrob) +* `#5837 `_ Corrected link to ConEmu (dritter) + +October, 2015 +------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#5345 `_ Adding information about empty files sent using BinaryFileResponse. (kherge) +* `#5214 `_ [WIP] Reworking most of the registration form: (weaverryan) +* `#5677 `_ replacing deprecated usage of True, False, Null validators in docs (Tim Stamp) +* `#5314 `_ Documented the useAttributeAsKey() method (javiereguiluz) +* `#5377 `_ Added a cookbook section about event subscribers (beni0888, javiereguiluz) +* `#5592 `_ Updated the article about data collectors (javiereguiluz) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#5795 `_ Fix typo in UserType class (Dorozhko-Anton) +* `#5758 `_ symlink issues with php-fpm (kendrick-k) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#5843 `_ Fixed the YAML syntax for service references (javiereguiluz) +* `#5797 `_ [Process] use ProcessFailedException instead of RuntimeException. (aitboudad) +* `#5812 `_ Remove duplicate and confusing info about testing error pages (carlos-granados) +* `#5821 `_ Minor fixes in the HttpFoundation introduction article (javiereguiluz) +* `#5822 `_ Fixed a syntax issue (javiereguiluz) +* `#5796 `_ Fix for #5783 (BenjaminPaap) +* `#5810 `_ Fixed a typo (javiereguiluz) +* `#5784 `_ Add fe80::1 (j-d) +* `#5799 `_ make file path consitent with other articles (OskarStark) +* `#5794 `_ Minor tweaks for the registration form article (javiereguiluz) +* `#5801 `_ namespace fix (OskarStark) +* `#5792 `_ [Cookbook][EventDispatcher] fix build (xabbuh) +* `#5787 `_ Definition Tweaks - see #5314 (weaverryan) +* `#5777 `_ Update links (thewilkybarkid) +* `#5775 `_ Misspelling (carlos-granados) +* `#5664 `_ Info about implicit session start (ThomasLandauer) +* `#5744 `_ translations have been removed from symfony.com (xabbuh) +* `#5771 `_ Remove not existing response constant (amansilla) +* `#5766 `_ Fixed two typos (ThomasLandauer) +* `#5733 `_ [Components][OptionsResolver] adding type hint to normalizer callback (xabbuh) +* `#5678 `_ Update HttpFoundation note after recent changes in routing component (senkal) +* `#5643 `_ Document how to customize the prototype (daFish, WouterJ) +* `#5584 `_ Add DebugBundle config reference (WouterJ) +* `#5753 `_ configureOptions(...) : protected => public (lucascherifi) +* `#5750 `_ fix YAML syntax highlighting (xabbuh) +* `#5749 `_ complete Swiftmailer XML examples (xabbuh) +* `#5726 `_ Document the support of Mintty for colors (stof) +* `#5708 `_ Added caution to call createView after handleRequest (WouterJ) +* `#5640 `_ Update controller.rst clarifying automatic deletion for flash messages (miguelvilata) +* `#5578 `_ Add supported branches in platform.sh section (WouterJ) +* `#5468 `_ [Cookbook][Templating] Add note about cache warming namespaced twig templates (kbond) +* `#5684 `_ Fix delivery_whitelist regex (gonzalovilaseca) +* `#5742 `_ incorrect: severity is an array key here and not a constant (lbayerl) + +September, 2015 +--------------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#5555 `_ added result yaml and xml from example code (OskarStark) +* `#5631 `_ Updated the Quick Tour to the latest changes introduced by Symfony (javiereguiluz) +* `#5497 `_ Simplified the Quick tour explanation about Symfony Installation (DQNEO) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#5629 `_ Fixing web user permission (BenoitLeveque) +* `#5673 `_ Update http_cache.rst (szyszka90) +* `#5666 `_ Fix EntityManager namespace (JhonnyL) +* `#5656 `_ Fix monolog line formatter in logging cookbook example. (vmarquez) +* `#5507 `_ Path fixed (carlosreig) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#5740 `_ Fix typo in PdoSessionHandler Documentation (tobemedia) +* `#5719 `_ changed repo names to the new ones (fabpot) +* `#5227 `_ [Cookbook] Fix doc on Generic Form Type Extensions (lemoinem) +* `#5703 `_ comment old logic (OskarStark) +* `#5683 `_ Improve the demo-warning. (GuGuss) +* `#5690 `_ Updated the release process image (javiereguiluz) +* `#5188 `_ Updated Cookies & Caching section (lukey78) +* `#5710 `_ Fix grammar mistake in security.rst (zatikbalazs) +* `#5706 `_ Update assetic.rst (Acinonux) +* `#5705 `_ Update assetic.rst (Acinonux) +* `#5685 `_ Fix indentation in some annotations (iamdto) +* `#5704 `_ Fix typo in translation.rst (zatikbalazs) +* `#5701 `_ Update testing.rst (hansallis) +* `#5711 `_ removed service call from controller (sloba88) +* `#5692 `_ Made a sentence slightly more english (GTheron) +* `#5715 `_ Add missing code tag (zatikbalazs) +* `#5720 `_ adding closing tag (InfoTracer) +* `#5714 `_ Remove unnecessary word from http_cache.rst (zatikbalazs) +* `#5680 `_ fix grammar mistake (greg0ire) +* `#5682 `_ Fix grammar and CS (iamdto) +* `#5652 `_ Do not use dynamic REQUEST_URI from $_SERVER as base url (senkal) +* `#5654 `_ Doc about new way of running tests (nicolas-grekas) +* `#5598 `_ [Cookbook][Security] proofread comments in voter article (xabbuh) +* `#5560 `_ [2.3] [Contributing] [CS] Added missing docblocks in code snippet (phansys) +* `#5674 `_ Update cookbook entries with best practices (JhonnyL) +* `#5675 `_ [Contributing] add a link to the testing section (xabbuh) +* `#5669 `_ Better explanation of implicit exception response status code (hvt) +* `#5651 `_ [Reference][Constraints] follow best practices in the constraints reference (xabbuh) +* `#5648 `_ Minor fixes for the QuestionHelper documentation (javiereguiluz) +* `#5641 `_ Move important information out of versionadded (WouterJ) +* `#5619 `_ Remove a caution note about StringUtils::equals() which is no longer true (javiereguiluz) +* `#5571 `_ Some small fixes for upload files article (WouterJ) +* `#5660 `_ Improved "Community Reviews" page (webmozart) + +August, 2015 +------------ + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#5480 `_ Added page "Community Reviews" (webmozart) +* `#5595 `_ Improve humanize filter documentation (bocharsky-bw) +* `#5319 `_ [Console] Command Lifecycle explications (94noni) +* `#5394 `_ Fix Major upgrade article for 2.7.1 changes (WouterJ) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#5589 `_ [Cookbook][Session] fix default expiry field name (xabbuh) +* `#5607 `_ Fix (sebastianbergmann) +* `#5608 `_ updated validation.rst (issei-m) +* `#5449 `_ Ensure that the entity is updated. (yceruto) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#5553 `_ Fix all broken links/permanent redirects/removed anchors (WouterJ) +* `#5650 `_ [RFR] fixing typo and removing duplicated lines in Config component doc (salahm) +* `#5635 `_ Fix minor problems in book/page_creation.rst (fabschurt) +* `#5647 `_ don't ignore the _exts directory anymore (xabbuh) +* `#5587 `_ [2.6] Don't use deprecated features (WouterJ) +* `#5637 `_ Add QueryBuilder vs DQL section (bocharsky-bw) +* `#5645 `_ Updated Constraint reference with best practices (WouterJ) +* `#5646 `_ Moved comment to the right place (mickaelandrieu) +* `#5649 `_ [RFR] Fixing typo in Symfony version for ButtonType (salahm) +* `#5606 `_ Use symfony.com theme on Platform.sh builds (WouterJ) +* `#5644 `_ Update page_creation.rst (jeromenadaud) +* `#5593 `_ Updated the profiler matchers article (javiereguiluz) +* `#5522 `_ [create_framework] Add missing extract() 2nd arg (kenjis) +* `#5597 `_ [CreateFramework] don't override existing variables (xabbuh) +* `#5628 `_ Updated the installation chapter (javiereguiluz) +* `#5638 `_ Update page_creation.rst (jeromenadaud) +* `#5636 `_ Fixed typo in web-assets.rst (nielsvermaut) +* `#5633 `_ Upgrade Platform.sh configuration snippet. (GuGuss) +* `#5620 `_ Changed the recommendation about the LICENSE file for third-party bundles (javiereguiluz) +* `#5617 `_ Add Body tag to see the web debug toolbar (rmed19) +* `#5594 `_ Missing --no-interaction flag? (alexwybraniec) +* `#5613 `_ Remove unneeded backtick (fabschurt) +* `#5622 `_ typo fix in pre authenticated (Maxime Douailin) +* `#5624 `_ the_architecture: Fix syntax error (kainjow) +* `#5609 `_ Add a missing backtick (fabschurt) +* `#5312 `_ Some fixes for bundle best practices (WouterJ) +* `#5601 `_ Update lazy_services.rst (baziak3) +* `#5591 `_ Update templating.rst: lint:twig instead of twig:lint in 2.7 (alexwybraniec) + +July, 2015 +---------- + +New Documentation +~~~~~~~~~~~~~~~~~ + +* `#5533 `_ Replace Capifony with Capistrano/symfony (mojzis) +* `#5543 `_ Add deprecation notice to "choice_list" option of ChoiceType (XitasoChris) +* `#5516 `_ Added a note about session data size in PdoSessionHandler (javiereguiluz) +* `#5499 `_ The "property" option of DoctrineType was deprecated. (XWB) +* `#5491 `_ added composer info (OskarStark) +* `#5478 `_ Add cookbook article for using MongoDB to store session data (stevenmusumeche) +* `#5472 `_ Added a tip about hashing the result of nextBytes() (javiereguiluz) +* `#5453 `_ Cleanup security voters cookbook recipes (WouterJ) +* `#5444 `_ Documented the "auto_alias" feature (javiereguiluz) +* `#5201 `_ [Book][Routing] Add example about how to match multiple methods (xelaris) +* `#5430 `_ Pr/5085 (sjagr, javiereguiluz) +* `#5456 `_ Completely re-reading the data transformers chapter (weaverryan) +* `#5426 `_ Documented the checkDNS option of the Url validator (saro0h, javiereguiluz) +* `#5333 `_ [FrameworkBundle] Update serializer configuration reference (dunglas) +* `#5424 `_ Integrate the "Create your own framework" tutorial (fabpot, lyrixx, jdreesen, catchamonkey, gnugat, andreia, Arnaud Kleinpeter, willdurand, amitayh, nanocom, hrbonz, Pedro Gimenez, ubick, dirkaholic, bamarni, revollat, javiereguiluz) + +Fixed Documentation +~~~~~~~~~~~~~~~~~~~ + +* `#5567 `_ Change Sql Field name because it's reserved (rmed19) +* `#5528 `_ [reate_framework] Fix mock $matcher (kenjis) +* `#5501 `_ Fix typo in url for PHPUnit test coverage report (TrueGit) +* `#5501 `_ Fix typo in url for PHPUnit test coverage report (TrueGit) +* `#5461 `_ Rework quick tour big picture (smatejic, DQNEO, xabbuh) +* `#5488 `_ fix #5487 (emillosanti) +* `#5496 `_ Security voters fixes (german.bortoli) +* `#5424 `_ Integrate the "Create your own framework" tutorial (fabpot, lyrixx, jdreesen, catchamonkey, gnugat, andreia, Arnaud Kleinpeter, willdurand, amitayh, nanocom, hrbonz, Pedro Gimenez, ubick, dirkaholic, bamarni, revollat, javiereguiluz) + +Minor Documentation Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `#5575 `_ Move some articles from wrong sections (sylvaincombes, WouterJ) +* `#5580 `_ Additional User check in voter class (weaverryan) +* `#5573 `_ fix YAML syntax highlighting (xabbuh) +* `#5564 `_ Improve and simplify the contributing instructions about tests (javiereguiluz) +* `#5550 `_ [docbot] Reviewed some component chapters (WouterJ) +* `#5556 `_ Fix typo Esi in part create framework (nicolasdewez) +* `#5568 `_ [Create Framework] Fix extract calls (replaces #5522) (kenjis) +* `#5548 `_ use the include() Twig function instead of the tag (xabbuh) +* `#5542 `_ [Cookbook][Email] add missing versionadded directive (xabbuh) +* `#5476 `_ [Cookbook][Security] some additional tweaks for the voter cookbook (xabbuh) +* `#5413 `_ Fix doc about deprecations policy (nicolas-grekas) +* `#5557 `_ [2.3] [Contributing] Added note about empty returns (phansys) +* `#5492 `_ updated tree for front controller (OskarStark) +* `#5536 `_ Removed reference to remove HTTPS off from nginx configuration (wjzijderveld) +* `#5545 `_ Misc. improvements in the Console component introduction (javiereguiluz) +* `#5512 `_ [Cookbook] Backport PSR-7 bridge docs to 2.3 (dunglas, weaverryan) +* `#5494 `_ updated tree (OskarStark) +* `#5490 `_ changed headline (OskarStark) +* `#5479 `_ Update http-foundation.rst (jezemery) +* `#5552 `_ rename $input to $greetInput (Xosofox) +* `#5544 `_ [components][expression_language] Fix the wrong constructor for SerializedParsedExpression (zerustech) +* `#5537 `_ Update design patter of Event Dispatcher (almacbe) +* `#5546 `_ A bunch of doc fixes again (WouterJ) +* `#5486 `_ review all Security code blocks (xabbuh) +* `#5538 `_ Update email.rst (TisLars) +* `#5529 `_ [Cookbook][upload_file] Fix :methods: to remove doubled braces (bicpi) +* `#5455 `_ Improve travis build speed (WouterJ) +* `#5442 `_ Improved the explanation about the verbosity levels of the console (javiereguiluz) +* `#5523 `_ Custom voter example, fix missing curly brace (snroki) +* `#5524 `_ TYPO: missing closing parantheses of the array (listerical85) +* `#5519 `_ Prepare Platform.sh configuration files. (GuGuss) +* `#5443 `_ Added a note about the implementation of the verbosity semantic methods (javiereguiluz) +* `#5518 `_ Minor grammar fix. (maxolasersquad) +* `#5520 `_ Fix RST (kenjis) +* `#5429 `_ Promote Symfony's builtin serializer instead of JMS (javiereguiluz) +* `#5427 `_ Cookbook grammar and style fixes (frne, javiereguiluz) +* `#5505 `_ [Cookbook][Form] some tweaks to the data transformers chapter (xabbuh) +* `#5352 `_ Update http_fundamentals.rst (wouthoekstra) +* `#5471 `_ Updated the Symfony Versions Roadmap image (javiereguiluz) +* `#5511 `_ [HttpKernel] Fix use statement (dunglas) +* `#5510 `_ [PSR-7] Fix Diactoros link (dunglas) +* `#5506 `_ Fixes small typo in data transformers cookbook (catchamonkey) +* `#5425 `_ Added a caution note about invoking other commands (kix, javiereguiluz) +* `#5367 `_ Split Security into Authentication & Authorization (iltar) +* `#5485 `_ Fix invalid phpunit URLs (norkunas) +* `#5473 `_ --dev is default and causes a warning (DQNEO) +* `#5474 `_ typo in components/translation/instruction.rst (beesofts) + June, 2015 ---------- @@ -108,7 +1103,6 @@ Minor Documentation Changes * `#5357 `_ [Form] Replace deprecated form_enctype by form_start (JMLamodiere) * `#5359 `_ Bumped version of proxy manager to stable release (peterrehm) - May, 2015 --------- @@ -202,7 +1196,6 @@ Minor Documentation Changes * `#5238 `_ Fixed typo and removed outdated imports (nomack84) * `#5240 `_ [Cookbook][Email] revert #4808 (xabbuh) - April, 2015 ----------- @@ -1392,7 +2385,7 @@ Fixed Documentation - `e385d28 `_ #3503 file extension correction xfliff to xliff (nixilla) - `6d34aa6 `_ #3478 Update custom_password_authenticator.rst (piotras-s) - `a171700 `_ #3477 Api key user provider should use "implements" instead of "extends" (skowi) -- `7fe0de3 `_ #3475 Fixed doc for framework.session.cookie_lifetime refrence. (tyomo4ka) +- `7fe0de3 `_ #3475 Fixed doc for framework.session.cookie_lifetime reference. (tyomo4ka) - `8155e4c `_ #3473 Update proxy_examples.rst (AZielinski) Minor Documentation Changes diff --git a/components/asset/introduction.rst b/components/asset.rst similarity index 89% rename from components/asset/introduction.rst rename to components/asset.rst index 40b51bf900e..fb0dfd30565 100644 --- a/components/asset/introduction.rst +++ b/components/asset.rst @@ -5,8 +5,11 @@ The Asset Component =================== - The Asset component manages URL generation and versioning of web assets such - as CSS stylesheets, JavaScript files and image files. + The Asset component manages URL generation and versioning of web assets such + as CSS stylesheets, JavaScript files and image files. + +.. versionadded:: 2.7 + The Asset component was introduced in Symfony 2.7. In the past, it was common for web applications to hardcode URLs of web assets. For example: @@ -42,10 +45,13 @@ simple. Hardcoding URLs can be a disadvantage because: Installation ------------ -You can install the component in two different ways: +.. code-block:: terminal + + $ composer require symfony/asset -* :doc:`Install it via Composer ` (``symfony/asset`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Asset). +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc Usage ----- @@ -111,13 +117,13 @@ suffix to any asset path:: In case you want to modify the version format, pass a sprintf-compatible format string as the second argument of the ``StaticVersionStrategy`` constructor:: - // put the 'version' word before the version value + // puts the 'version' word before the version value $package = new Package(new StaticVersionStrategy('v1', '%s?version=%s')); echo $package->getUrl('/image.png'); // result: /image.png?version=v1 - // put the asset version before its path + // puts the asset version before its path $package = new Package(new StaticVersionStrategy('v1', '%2$s/%1$s')); echo $package->getUrl('/image.png'); @@ -166,15 +172,15 @@ that path over and over again:: use Symfony\Component\Asset\PathPackage; // ... - $package = new PathPackage('/static/images', new StaticVersionStrategy('v1')); + $pathPackage = new PathPackage('/static/images', new StaticVersionStrategy('v1')); - echo $package->getUrl('/logo.png'); + echo $pathPackage->getUrl('/logo.png'); // result: /static/images/logo.png?v1 Request Context Aware Assets ............................ -If you are also using the :doc:`HttpFoundation ` +If you are also using the :doc:`HttpFoundation ` component in your project (for instance, in a Symfony application), the ``PathPackage`` class can take into account the context of the current request:: @@ -182,13 +188,13 @@ class can take into account the context of the current request:: use Symfony\Component\Asset\Context\RequestStackContext; // ... - $package = new PathPackage( + $pathPackage = new PathPackage( '/static/images', new StaticVersionStrategy('v1'), new RequestStackContext($requestStack) ); - echo $package->getUrl('/logo.png'); + echo $pathPackage->getUrl('/logo.png'); // result: /somewhere/static/images/logo.png?v1 Now that the request context is set, the ``PathPackage`` will prepend the @@ -209,12 +215,12 @@ class to generate absolute URLs for their assets:: use Symfony\Component\Asset\UrlPackage; // ... - $package = new UrlPackage( + $urlPackage = new UrlPackage( 'http://static.example.com/images/', new StaticVersionStrategy('v1') ); - echo $package->getUrl('/logo.png'); + echo $urlPackage->getUrl('/logo.png'); // result: http://static.example.com/images/logo.png?v1 You can also pass a schema-agnostic URL:: @@ -222,12 +228,12 @@ You can also pass a schema-agnostic URL:: use Symfony\Component\Asset\UrlPackage; // ... - $package = new UrlPackage( + $urlPackage = new UrlPackage( '//static.example.com/images/', new StaticVersionStrategy('v1') ); - echo $package->getUrl('/logo.png'); + echo $urlPackage->getUrl('/logo.png'); // result: //static.example.com/images/logo.png?v1 This is useful because assets will automatically be requested via HTTPS if @@ -245,11 +251,11 @@ constructor:: '//static1.example.com/images/', '//static2.example.com/images/', ); - $package = new UrlPackage($urls, new StaticVersionStrategy('v1')); + $urlPackage = new UrlPackage($urls, new StaticVersionStrategy('v1')); - echo $package->getUrl('/logo.png'); + echo $urlPackage->getUrl('/logo.png'); // result: http://static1.example.com/images/logo.png?v1 - echo $package->getUrl('/icon.png'); + echo $urlPackage->getUrl('/icon.png'); // result: http://static2.example.com/images/icon.png?v1 For each asset, one of the URLs will be randomly used. But, the selection @@ -268,13 +274,13 @@ protocol-relative URLs for HTTPs requests, any base URL for HTTP requests):: use Symfony\Component\Asset\Context\RequestStackContext; // ... - $package = new UrlPackage( + $urlPackage = new UrlPackage( array('http://example.com/', 'https://example.com/'), new StaticVersionStrategy('v1'), new RequestStackContext($requestStack) ); - echo $package->getUrl('/logo.png'); + echo $urlPackage->getUrl('/logo.png'); // assuming the RequestStackContext says that we are on a secure host // result: https://example.com/logo.png?v1 @@ -321,4 +327,7 @@ document inside a template:: echo $packages->getUrl('/resume.pdf', 'doc'); // result: /somewhere/deep/for/documents/resume.pdf?v1 +Learn more +---------- + .. _Packagist: https://packagist.org/packages/symfony/asset diff --git a/components/asset/index.rst b/components/asset/index.rst deleted file mode 100644 index 14d04b7eb6f..00000000000 --- a/components/asset/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Asset -===== - -.. toctree:: - :maxdepth: 2 - - introduction diff --git a/components/browser_kit.rst b/components/browser_kit.rst new file mode 100644 index 00000000000..e6e469196ce --- /dev/null +++ b/components/browser_kit.rst @@ -0,0 +1,246 @@ +.. index:: + single: BrowserKit + single: Components; BrowserKit + +The BrowserKit Component +======================== + + The BrowserKit component simulates the behavior of a web browser, allowing + you to make requests, click on links and submit forms programmatically. + +.. note:: + + The BrowserKit component can only make internal requests to your application. + If you need to make requests to external sites and applications, consider + using `Goutte`_, a simple web scraper based on Symfony Components. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/browser-kit + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +Basic Usage +----------- + +Creating a Client +~~~~~~~~~~~~~~~~~ + +The component only provides an abstract client and does not provide any backend +ready to use for the HTTP layer. + +To create your own client, you must extend the abstract ``Client`` class and +implement the :method:`Symfony\\Component\\BrowserKit\\Client::doRequest` method. +This method accepts a request and should return a response:: + + namespace Acme; + + use Symfony\Component\BrowserKit\Client as BaseClient; + use Symfony\Component\BrowserKit\Response; + + class Client extends BaseClient + { + protected function doRequest($request) + { + // ... convert request into a response + + return new Response($content, $status, $headers); + } + } + +For a simple implementation of a browser based on the HTTP layer, have a look +at `Goutte`_. For an implementation based on ``HttpKernelInterface``, have +a look at the :class:`Symfony\\Component\\HttpKernel\\Client` provided by +the :doc:`HttpKernel component `. + +Making Requests +~~~~~~~~~~~~~~~ + +Use the :method:`Symfony\\Component\\BrowserKit\\Client::request` method to +make HTTP requests. The first two arguments are the HTTP method and the requested +URL:: + + use Acme\Client; + + $client = new Client(); + $crawler = $client->request('GET', '/'); + +The value returned by the ``request()`` method is an instance of the +:class:`Symfony\\Component\\DomCrawler\\Crawler` class, provided by the +:doc:`DomCrawler component `, which allows accessing +and traversing HTML elements programmatically. + +Clicking Links +~~~~~~~~~~~~~~ + +The ``Crawler`` object is capable of simulating link clicks. First, pass the +text content of the link to the ``selectLink()`` method, which returns a +``Link`` object. Then, pass this object to the ``click()`` method, which +performs the needed HTTP GET request to simulate the link click:: + + use Acme\Client; + + $client = new Client(); + $crawler = $client->request('GET', '/product/123'); + $link = $crawler->selectLink('Go elsewhere...')->link(); + $client->click($link); + +Submitting Forms +~~~~~~~~~~~~~~~~ + +The ``Crawler`` object is also capable of selecting forms. First, select any of +the form's buttons with the ``selectButton()`` method. Then, use the ``form()`` +method to select the form which the button belongs to. + +After selecting the form, fill in its data and send it using the ``submit()`` +method (which makes the needed HTTP POST request to submit the form contents):: + + use Acme\Client; + + // make a real request to an external site + $client = new Client(); + $crawler = $client->request('GET', 'https://github.com/login'); + + // select the form and fill in some values + $form = $crawler->selectButton('Log in')->form(); + $form['login'] = 'symfonyfan'; + $form['password'] = 'anypass'; + + // To upload a file, the value should be the absolute file path + $form['file'] = __FILE__; + + // submit that form + $crawler = $client->submit($form); + +Cookies +------- + +Retrieving Cookies +~~~~~~~~~~~~~~~~~~ + +The ``Client`` implementation exposes cookies (if any) through a +:class:`Symfony\\Component\\BrowserKit\\CookieJar`, which allows you to store and +retrieve any cookie while making requests with the client:: + + use Acme\Client; + + // Make a request + $client = new Client(); + $crawler = $client->request('GET', '/'); + + // Get the cookie Jar + $cookieJar = $client->getCookieJar(); + + // Get a cookie by name + $cookie = $cookieJar->get('name_of_the_cookie'); + + // Get cookie data + $name = $cookie->getName(); + $value = $cookie->getValue(); + $rawValue = $cookie->getRawValue(); + $isSecure = $cookie->isSecure(); + $isHttpOnly = $cookie->isHttpOnly(); + $isExpired = $cookie->isExpired(); + $expires = $cookie->getExpiresTime(); + $path = $cookie->getPath(); + $domain = $cookie->getDomain(); + +.. note:: + + These methods only return cookies that have not expired. + +Looping Through Cookies +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + use Acme\Client; + + // Make a request + $client = new Client(); + $crawler = $client->request('GET', '/'); + + // Get the cookie Jar + $cookieJar = $client->getCookieJar(); + + // Get array with all cookies + $cookies = $cookieJar->all(); + foreach ($cookies as $cookie) { + // ... + } + + // Get all values + $values = $cookieJar->allValues('http://symfony.com'); + foreach ($values as $value) { + // ... + } + + // Get all raw values + $rawValues = $cookieJar->allRawValues('http://symfony.com'); + foreach ($rawValues as $rawValue) { + // ... + } + +Setting Cookies +~~~~~~~~~~~~~~~ + +You can also create cookies and add them to a cookie jar that can be injected +into the client constructor:: + + use Acme\Client; + + // create cookies and add to cookie jar + $cookie = new Cookie('flavor', 'chocolate', strtotime('+1 day')); + $cookieJar = new CookieJar(); + $cookieJar->set($cookie); + + // create a client and set the cookies + $client = new Client(array(), null, $cookieJar); + // ... + +History +------- + +The client stores all your requests allowing you to go back and forward in your +history:: + + use Acme\Client; + + $client = new Client(); + $client->request('GET', '/'); + + // select and click on a link + $link = $crawler->selectLink('Documentation')->link(); + $client->click($link); + + // go back to home page + $crawler = $client->back(); + + // go forward to documentation page + $crawler = $client->forward(); + +You can delete the client's history with the ``restart()`` method. This will +also delete all the cookies:: + + use Acme\Client; + + $client = new Client(); + $client->request('GET', '/'); + + // reset the client (history and cookies are cleared too) + $client->restart(); + +Learn more +---------- + +* :doc:`/testing` +* :doc:`/components/css_selector` +* :doc:`/components/dom_crawler` + +.. _`Packagist`: https://packagist.org/packages/symfony/browser-kit +.. _`Goutte`: https://github.com/FriendsOfPHP/Goutte diff --git a/components/class_loader/introduction.rst b/components/class_loader.rst similarity index 53% rename from components/class_loader/introduction.rst rename to components/class_loader.rst index 893859616e0..3a7021e8738 100644 --- a/components/class_loader/introduction.rst +++ b/components/class_loader.rst @@ -7,18 +7,24 @@ The ClassLoader Component The ClassLoader component provides tools to autoload your classes and cache their locations for performance. +.. caution:: + + The ClassLoader component was deprecated in Symfony 3.3 and it will be + removed in 4.0. As an alternative, use Composer's class loading mechanism. + Usage ----- Whenever you reference a class that has not been required or included yet, -PHP uses the `autoloading mechanism`_ to delegate the loading of a file defining -the class. Symfony provides three autoloaders, which are able to load your classes: +PHP uses the `autoloading mechanism`_ to delegate the loading of a file +defining the class. Symfony provides three autoloaders, which are able to +load your classes: * :doc:`/components/class_loader/class_loader`: loads classes that follow - the `PSR-0` class naming standard; + the `PSR-0`_ class naming standard; * :doc:`/components/class_loader/psr4_class_loader`: loads classes that follow - the `PSR-4` class naming standard; + the `PSR-4`_ class naming standard; * :doc:`/components/class_loader/map_class_loader`: loads classes using a static map from class name to file path. @@ -27,21 +33,33 @@ Additionally, the Symfony ClassLoader component ships with a wrapper class which makes it possible :doc:`to cache the results of a class loader `. -When using the :doc:`Debug component `, you -can also use a special :doc:`DebugClassLoader ` +When using the :doc:`Debug component `, you +can also use a special :ref:`DebugClassLoader ` that eases debugging by throwing more helpful exceptions when a class could not be found by a class loader. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal + + $ composer require symfony/class-loader -* :doc:`Install it via Composer ` (``symfony/class-loader`` - on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/ClassLoader). +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc -.. _`autoloading mechanism`: http://php.net/manual/en/language.oop5.autoload.php +Learn More +---------- + +.. toctree:: + :glob: + :maxdepth: 1 + + class_loader/class_loader + class_loader/* + +.. _PSR-0: https://www.php-fig.org/psr/psr-0/ +.. _PSR-4: https://www.php-fig.org/psr/psr-4/ +.. _`autoloading mechanism`: https://php.net/manual/en/language.oop5.autoload.php .. _Packagist: https://packagist.org/packages/symfony/class-loader diff --git a/components/class_loader/cache_class_loader.rst b/components/class_loader/cache_class_loader.rst index ff7aaba0885..cb0d1dd37d1 100644 --- a/components/class_loader/cache_class_loader.rst +++ b/components/class_loader/cache_class_loader.rst @@ -8,9 +8,6 @@ Cache a Class Loader ==================== -Introduction ------------- - Finding the file for a particular class can be an expensive task. Luckily, the ClassLoader component comes with two classes to cache the mapping from a class to its containing file. Both the :class:`Symfony\\Component\\ClassLoader\\ApcClassLoader` @@ -37,10 +34,10 @@ ApcClassLoader // sha1(__FILE__) generates an APC namespace prefix $cachedLoader = new ApcClassLoader(sha1(__FILE__), $loader); - // register the cached class loader + // registers the cached class loader $cachedLoader->register(); - // deactivate the original, non-cached loader if it was registered previously + // deactivates the original, non-cached loader if it was registered previously $loader->unregister(); XcacheClassLoader @@ -57,12 +54,12 @@ it is straightforward:: // sha1(__FILE__) generates an XCache namespace prefix $cachedLoader = new XcacheClassLoader(sha1(__FILE__), $loader); - // register the cached class loader + // registers the cached class loader $cachedLoader->register(); - // deactivate the original, non-cached loader if it was registered previously + // deactivates the original, non-cached loader if it was registered previously $loader->unregister(); -.. _APC: http://php.net/manual/en/book.apc.php +.. _APC: https://php.net/manual/en/book.apc.php .. _autoloader: https://getcomposer.org/doc/01-basic-usage.md#autoloading -.. _XCache: http://xcache.lighttpd.net +.. _XCache: https://xcache.lighttpd.net diff --git a/components/class_loader/class_loader.rst b/components/class_loader/class_loader.rst index 1bef3f652d9..c6d37661a84 100644 --- a/components/class_loader/class_loader.rst +++ b/components/class_loader/class_loader.rst @@ -4,14 +4,14 @@ The PSR-0 Class Loader ====================== -If your classes and third-party libraries follow the `PSR-0`_ standard, you -can use the :class:`Symfony\\Component\\ClassLoader\\ClassLoader` class to -load all of your project's classes. +If your classes and third-party libraries follow the `PSR-0`_ standard, +you can use the :class:`Symfony\\Component\\ClassLoader\\ClassLoader` class +to load all of your project's classes. .. tip:: - You can use both the ``ApcClassLoader`` and the ``XcacheClassLoader`` to - :doc:`cache ` a ``ClassLoader`` + You can use both the ``ApcClassLoader`` and the ``XcacheClassLoader`` + to :doc:`cache ` a ``ClassLoader`` instance. Usage @@ -33,25 +33,20 @@ is straightforward:: $loader->register(); -.. note:: - - The autoloader is automatically registered in a Symfony application (see - ``app/autoload.php``). - -Use the :method:`Symfony\\Component\\ClassLoader\\ClassLoader::addPrefix` or -:method:`Symfony\\Component\\ClassLoader\\ClassLoader::addPrefixes` methods to -register your classes:: +Use :method:`Symfony\\Component\\ClassLoader\\ClassLoader::addPrefix` or +:method:`Symfony\\Component\\ClassLoader\\ClassLoader::addPrefixes` to register +your classes:: // register a single namespaces $loader->addPrefix('Symfony', __DIR__.'/vendor/symfony/symfony/src'); - // register several namespaces at once + // registers several namespaces at once $loader->addPrefixes(array( 'Symfony' => __DIR__.'/../vendor/symfony/symfony/src', 'Monolog' => __DIR__.'/../vendor/monolog/monolog/src', )); - // register a prefix for a class following the PEAR naming conventions + // registers a prefix for a class following the PEAR naming conventions $loader->addPrefix('Twig_', __DIR__.'/vendor/twig/twig/lib'); $loader->addPrefixes(array( @@ -59,22 +54,22 @@ register your classes:: 'Twig_' => __DIR__.'/vendor/twig/twig/lib', )); -Classes from a sub-namespace or a sub-hierarchy of `PEAR`_ classes can be looked -for in a location list to ease the vendoring of a sub-set of classes for large -projects:: +Classes from a sub-namespace or a sub-hierarchy of `PEAR`_ classes can be +looked for in a location list to ease the vendoring of a sub-set of classes +for large projects:: $loader->addPrefixes(array( - 'Doctrine\\Common' => __DIR__.'/vendor/doctrine/common/lib', - 'Doctrine\\DBAL\\Migrations' => __DIR__.'/vendor/doctrine/migrations/lib', - 'Doctrine\\DBAL' => __DIR__.'/vendor/doctrine/dbal/lib', - 'Doctrine' => __DIR__.'/vendor/doctrine/orm/lib', + 'Doctrine\Common' => __DIR__.'/vendor/doctrine/common/lib', + 'Doctrine\DBAL\Migrations' => __DIR__.'/vendor/doctrine/migrations/lib', + 'Doctrine\DBAL' => __DIR__.'/vendor/doctrine/dbal/lib', + 'Doctrine' => __DIR__.'/vendor/doctrine/orm/lib', )); In this example, if you try to use a class in the ``Doctrine\Common`` namespace -or one of its children, the autoloader will first look for the class under the -``doctrine-common`` directory. If not found, it will then fallback to the default -``Doctrine`` directory (the last one configured) before giving up. The order -of the prefix registrations is significant in this case. +or one of its children, the autoloader will first look for the class under +the ``doctrine-common`` directory. If not found, it will then fallback to +the default ``Doctrine`` directory (the last one configured) before giving +up. The order of the prefix registrations is significant in this case. -.. _PEAR: http://pear.php.net/manual/en/standards.naming.php -.. _PSR-0: http://www.php-fig.org/psr/psr-0/ +.. _PEAR: https://pear.php.net/manual/en/standards.naming.php +.. _PSR-0: https://www.php-fig.org/psr/psr-0/ diff --git a/components/class_loader/class_map_generator.rst b/components/class_loader/class_map_generator.rst index 772bd9ab693..c060cc71a72 100644 --- a/components/class_loader/class_map_generator.rst +++ b/components/class_loader/class_map_generator.rst @@ -5,10 +5,11 @@ The Class Map Generator ======================= -Loading a class usually is an easy task given the `PSR-0`_ and `PSR-4`_ standards. -Thanks to the Symfony ClassLoader component or the autoloading mechanism provided -by Composer, you don't have to map your class names to actual PHP files manually. -Nowadays, PHP libraries usually come with autoloading support through Composer. +Loading a class usually is an easy task given the `PSR-0`_ and `PSR-4`_ +standards. Thanks to the Symfony ClassLoader component or the autoloading +mechanism provided by Composer, you don't have to map your class names to +actual PHP files manually. Nowadays, PHP libraries usually come with autoloading +support through Composer. But from time to time you may have to use a third-party library that comes without any autoloading support and therefore forces you to load each class @@ -44,16 +45,17 @@ it possible to create a map of class names to files. Generating a Class Map ---------------------- -To generate the class map, simply pass the root directory of your class files -to the :method:`Symfony\\Component\\ClassLoader\\ClassMapGenerator::createMap` +To generate the class map, simply pass the root directory of your class +files to the +:method:`Symfony\\Component\\ClassLoader\\ClassMapGenerator::createMap` method:: use Symfony\Component\ClassLoader\ClassMapGenerator; var_dump(ClassMapGenerator::createMap(__DIR__.'/library')); -Given the files and class from the table above, you should see an output like -this: +Given the files and class from the table above, you should see an output +like this: .. code-block:: text @@ -87,8 +89,9 @@ file in the same directory with the following contents:: 'Acme\\Bar' => '/var/www/library/bar/Foo.php', ); -Instead of loading each file manually, you'll only have to register the generated -class map with, for example, the :class:`Symfony\\Component\\ClassLoader\\MapClassLoader`:: +Instead of loading each file manually, you'll only have to register the +generated class map with, for example, the +:class:`Symfony\\Component\\ClassLoader\\MapClassLoader`:: use Symfony\Component\ClassLoader\MapClassLoader; @@ -110,8 +113,8 @@ class map with, for example, the :class:`Symfony\\Component\\ClassLoader\\MapCla component. Besides dumping the class map for one directory, you can also pass an array -of directories for which to generate the class map (the result actually is -the same as in the example above):: +of directories for which to generate the class map (the result actually +is the same as in the example above):: use Symfony\Component\ClassLoader\ClassMapGenerator; @@ -120,6 +123,6 @@ the same as in the example above):: __DIR__.'/class_map.php' ); -.. _`PSR-0`: http://www.php-fig.org/psr/psr-0 -.. _`PSR-4`: http://www.php-fig.org/psr/psr-4 +.. _`PSR-0`: https://www.php-fig.org/psr/psr-0 +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4 .. _`Composer`: https://getcomposer.org diff --git a/components/class_loader/debug_class_loader.rst b/components/class_loader/debug_class_loader.rst index d41afe9d277..e316eeb8084 100644 --- a/components/class_loader/debug_class_loader.rst +++ b/components/class_loader/debug_class_loader.rst @@ -5,4 +5,4 @@ Debugging a Class Loader The ``DebugClassLoader`` from the ClassLoader component was deprecated in Symfony 2.5 and will be removed in Symfony 3.0. Use the - :doc:`DebugClassLoader provided by the Debug component `. + :ref:`DebugClassLoader provided by the Debug component `. diff --git a/components/class_loader/index.rst b/components/class_loader/index.rst deleted file mode 100644 index 5215b57291d..00000000000 --- a/components/class_loader/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -ClassLoader -=========== - -.. toctree:: - :maxdepth: 2 - - introduction - class_loader - psr4_class_loader - map_class_loader - cache_class_loader - class_map_generator - -.. toctree:: - :hidden: - - debug_class_loader diff --git a/components/class_loader/map_class_loader.rst b/components/class_loader/map_class_loader.rst index 2705fa24282..e6e464e771e 100644 --- a/components/class_loader/map_class_loader.rst +++ b/components/class_loader/map_class_loader.rst @@ -4,20 +4,22 @@ MapClassLoader ============== -The :class:`Symfony\\Component\\ClassLoader\\MapClassLoader` allows you to -autoload files via a static map from classes to files. This is useful if you -use third-party libraries which don't follow the `PSR-0`_ standards and so -can't use the :doc:`PSR-0 class loader `. +The :class:`Symfony\\Component\\ClassLoader\\MapClassLoader` allows you +to autoload files via a static map from classes to files. This is useful +if you use third-party libraries which don't follow the `PSR-0`_ standards +and so can't use the +:doc:`PSR-0 class loader `. -The ``MapClassLoader`` can be used along with the :doc:`PSR-0 class loader ` -by configuring and calling the ``register()`` method on both. +The ``MapClassLoader`` can be used along with the +:doc:`PSR-0 class loader ` by +configuring and calling the ``register()`` method on both. .. note:: The default behavior is to append the ``MapClassLoader`` on the autoload - stack. If you want to use it as the first autoloader, pass ``true`` when - calling the ``register()`` method. Your class loader will then be prepended - on the autoload stack. + stack. If you want to use it as the first autoloader, pass ``true`` + when calling the ``register()`` method. Your class loader will then + be prepended on the autoload stack. Usage ----- @@ -36,4 +38,4 @@ an instance of the ``MapClassLoader`` class:: $loader->register(); -.. _PSR-0: http://www.php-fig.org/psr/psr-0/ +.. _PSR-0: https://www.php-fig.org/psr/psr-0/ diff --git a/components/class_loader/psr4_class_loader.rst b/components/class_loader/psr4_class_loader.rst index b593e174027..09d0af057e9 100644 --- a/components/class_loader/psr4_class_loader.rst +++ b/components/class_loader/psr4_class_loader.rst @@ -38,9 +38,7 @@ The directory structure will look like this: demo.php In ``demo.php`` you are going to parse the ``config.yml`` file. To do that, you -first need to configure the ``Psr4ClassLoader``: - -.. code-block:: php +first need to configure the ``Psr4ClassLoader``:: use Symfony\Component\ClassLoader\Psr4ClassLoader; use Symfony\Component\Yaml\Yaml; @@ -48,7 +46,7 @@ first need to configure the ``Psr4ClassLoader``: require __DIR__.'/lib/ClassLoader/Psr4ClassLoader.php'; $loader = new Psr4ClassLoader(); - $loader->addPrefix('Symfony\\Component\\Yaml\\', __DIR__.'/lib/Yaml'); + $loader->addPrefix('Symfony\Component\Yaml\\', __DIR__.'/lib/Yaml'); $loader->register(); $data = Yaml::parse(file_get_contents(__DIR__.'/config.yml')); @@ -60,4 +58,4 @@ tell the class loader where to look for classes with the ``Symfony\Component\Yaml\`` namespace prefix. After registering the autoloader, the Yaml component is ready to be used. -.. _PSR-4: http://www.php-fig.org/psr/psr-4/ +.. _PSR-4: https://www.php-fig.org/psr/psr-4/ diff --git a/components/config/introduction.rst b/components/config.rst similarity index 57% rename from components/config/introduction.rst rename to components/config.rst index 70f74c2072f..8134007103d 100644 --- a/components/config/introduction.rst +++ b/components/config.rst @@ -12,18 +12,24 @@ The Config Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/config`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Config). + $ composer require symfony/config + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc -Sections --------- +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: -* :doc:`/components/config/resources` -* :doc:`/components/config/caching` -* :doc:`/components/config/definition` + config/* + /bundles/configuration + /bundles/extension + /bundles/prepend_extension .. _Packagist: https://packagist.org/packages/symfony/config diff --git a/components/config/caching.rst b/components/config/caching.rst index 4bb43558c64..4984a59d755 100644 --- a/components/config/caching.rst +++ b/components/config/caching.rst @@ -1,26 +1,27 @@ .. index:: single: Config; Caching based on resources -Caching Based on Resources +Caching based on Resources ========================== -When all configuration resources are loaded, you may want to process the configuration -values and combine them all in one file. This file acts like a cache. Its -contents don’t have to be regenerated every time the application runs – only -when the configuration resources are modified. +When all configuration resources are loaded, you may want to process the +configuration values and combine them all in one file. This file acts +like a cache. Its contents don’t have to be regenerated every time the +application runs – only when the configuration resources are modified. For example, the Symfony Routing component allows you to load all routes, and then dump a URL matcher or a URL generator based on these routes. In -this case, when one of the resources is modified (and you are working in a -development environment), the generated file should be invalidated and regenerated. -This can be accomplished by making use of the :class:`Symfony\\Component\\Config\\ConfigCache` -class. - -The example below shows you how to collect resources, then generate some code -based on the resources that were loaded, and write this code to the cache. The -cache also receives the collection of resources that were used for generating -the code. By looking at the "last modified" timestamp of these resources, -the cache can tell if it is still fresh or that its contents should be regenerated:: +this case, when one of the resources is modified (and you are working +in a development environment), the generated file should be invalidated +and regenerated. This can be accomplished by making use of the +:class:`Symfony\\Component\\Config\\ConfigCache` class. + +The example below shows you how to collect resources, then generate some +code based on the resources that were loaded and write this code to the +cache. The cache also receives the collection of resources that were used +for generating the code. By looking at the "last modified" timestamp of +these resources, the cache can tell if it is still fresh or that its contents +should be regenerated:: use Symfony\Component\Config\ConfigCache; use Symfony\Component\Config\Resource\FileResource; @@ -52,8 +53,8 @@ the cache can tell if it is still fresh or that its contents should be regenerat // you may want to require the cached code: require $cachePath; -In debug mode, a ``.meta`` file will be created in the same directory as the -cache file itself. This ``.meta`` file contains the serialized resources, -whose timestamps are used to determine if the cache is still fresh. When not -in debug mode, the cache is considered to be "fresh" as soon as it exists, +In debug mode, a ``.meta`` file will be created in the same directory as +the cache file itself. This ``.meta`` file contains the serialized resources, +whose timestamps are used to determine if the cache is still fresh. When +not in debug mode, the cache is considered to be "fresh" as soon as it exists, and therefore no ``.meta`` file will be generated. diff --git a/components/config/definition.rst b/components/config/definition.rst index 27beb52e4d6..51a56628a89 100644 --- a/components/config/definition.rst +++ b/components/config/definition.rst @@ -8,12 +8,13 @@ Validating Configuration Values ------------------------------- After loading configuration values from all kinds of resources, the values -and their structure can be validated using the "Definition" part of the Config -Component. Configuration values are usually expected to show some kind of -hierarchy. Also, values should be of a certain type, be restricted in number -or be one of a given set of values. For example, the following configuration -(in YAML) shows a clear hierarchy and some validation rules that should be -applied to it (like: "the value for ``auto_connect`` must be a boolean value"): +and their structure can be validated using the "Definition" part of the +Config Component. Configuration values are usually expected to show some +kind of hierarchy. Also, values should be of a certain type, be restricted +in number or be one of a given set of values. For example, the following +configuration (in YAML) shows a clear hierarchy and some validation rules +that should be applied to it (like: "the value for ``auto_connect`` must +be a boolean value"): .. code-block:: yaml @@ -44,9 +45,9 @@ Defining a Hierarchy of Configuration Values Using the TreeBuilder All the rules concerning configuration values can be defined using the :class:`Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder`. -A :class:`Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder` instance -should be returned from a custom ``Configuration`` class which implements the -:class:`Symfony\\Component\\Config\\Definition\\ConfigurationInterface`:: +A :class:`Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder` +instance should be returned from a custom ``Configuration`` class which +implements the :class:`Symfony\\Component\\Config\\Definition\\ConfigurationInterface`:: namespace Acme\DatabaseConfiguration; @@ -89,7 +90,8 @@ reflect the real structure of the configuration values:: The root node itself is an array node, and has children, like the boolean node ``auto_connect`` and the scalar node ``default_connection``. In general: -after defining a node, a call to ``end()`` takes you one step up in the hierarchy. +after defining a node, a call to ``end()`` takes you one step up in the +hierarchy. Node Type ~~~~~~~~~ @@ -97,7 +99,8 @@ Node Type It is possible to validate the type of a provided value by using the appropriate node definition. Node types are available for: -* scalar (generic type that includes booleans, strings, integers, floats and ``null``) +* scalar (generic type that includes booleans, strings, integers, floats + and ``null``) * boolean * integer * float @@ -112,9 +115,9 @@ Numeric Node Constraints ~~~~~~~~~~~~~~~~~~~~~~~~ Numeric nodes (float and integer) provide two extra constraints - -:method:`Symfony\\Component\\Config\\Definition\\Builder::min` and -:method:`Symfony\\Component\\Config\\Definition\\Builder::max` - -allowing to validate the value:: +:method:`Symfony\\Component\\Config\\Definition\\Builder\\IntegerNodeDefinition::min` +and :method:`Symfony\\Component\\Config\\Definition\\Builder\\IntegerNodeDefinition::max` +- allowing to validate the value:: $rootNode ->children() @@ -138,13 +141,14 @@ values:: $rootNode ->children() - ->enumNode('gender') - ->values(array('male', 'female')) + ->enumNode('delivery') + ->values(array('standard', 'expedited', 'priority')) ->end() ->end() ; -This will restrict the ``gender`` option to be either ``male`` or ``female``. +This will restrict the ``delivery`` options to be either ``standard``, +``expedited`` or ``priority``. Array Nodes ~~~~~~~~~~~ @@ -193,44 +197,170 @@ Array Node Options Before defining the children of an array node, you can provide options like: ``useAttributeAsKey()`` - Provide the name of a child node, whose value should be used as the key in the resulting array. + Provide the name of a child node, whose value should be used as the key in + the resulting array. This method also defines the way config array keys are + treated, as explained in the following example. ``requiresAtLeastOneElement()`` - There should be at least one element in the array (works only when ``isRequired()`` is also - called). + There should be at least one element in the array (works only when + ``isRequired()`` is also called). ``addDefaultsIfNotSet()`` - If any child nodes have default values, use them if explicit values haven't been provided. + If any child nodes have default values, use them if explicit values + haven't been provided. +``normalizeKeys(false)`` + If called (with ``false``), keys with dashes are *not* normalized to underscores. + It is recommended to use this with prototype nodes where the user will define + a key-value map, to avoid an unnecessary transformation. -An example of this:: +A basic prototyped array configuration can be defined as follows:: - $rootNode + $node + ->fixXmlConfig('driver') ->children() - ->arrayNode('parameters') - ->isRequired() - ->requiresAtLeastOneElement() - ->useAttributeAsKey('name') + ->arrayNode('drivers') + ->prototype('scalar')->end() + ->end() + ->end() + ; + +When using the following YAML configuration: + +.. code-block:: yaml + + drivers: ['mysql', 'sqlite'] + +Or the following XML configuration: + +.. code-block:: xml + + mysql + sqlite + +The processed configuration is:: + + Array( + [0] => 'mysql' + [1] => 'sqlite' + ) + +A more complex example would be to define a prototyped array with children:: + + $node + ->fixXmlConfig('connection') + ->children() + ->arrayNode('connections') ->prototype('array') ->children() - ->scalarNode('value')->isRequired()->end() + ->scalarNode('table')->end() + ->scalarNode('user')->end() + ->scalarNode('password')->end() ->end() ->end() ->end() ->end() ; -In YAML, the configuration might look like this: +When using the following YAML configuration: .. code-block:: yaml - database: - parameters: - param1: { value: param1val } + connections: + - { table: symfony, user: root, password: ~ } + - { table: foo, user: root, password: pa$$ } + +Or the following XML configuration: + +.. code-block:: xml + + + + +The processed configuration is:: + + Array( + [0] => Array( + [table] => 'symfony' + [user] => 'root' + [password] => null + ) + [1] => Array( + [table] => 'foo' + [user] => 'root' + [password] => 'pa$$' + ) + ) -In XML, each ``parameters`` node would have a ``name`` attribute (along with -``value``), which would be removed and used as the key for that element in -the final array. The ``useAttributeAsKey`` is useful for normalizing how -arrays are specified between different formats like XML and YAML. +The previous output matches the expected result. However, given the configuration +tree, when using the following YAML configuration: -Default and required Values +.. code-block:: yaml + + connections: + sf_connection: + table: symfony + user: root + password: ~ + default: + table: foo + user: root + password: pa$$ + +The output configuration will be exactly the same as before. In other words, the +``sf_connection`` and ``default`` configuration keys are lost. The reason is that +the Symfony Config component treats arrays as lists by default. + +.. note:: + + As of writing this, there is an inconsistency: if only one file provides the + configuration in question, the keys (i.e. ``sf_connection`` and ``default``) + are *not* lost. But if more than one file provides the configuration, the keys + are lost as described above. + +In order to maintain the array keys use the ``useAttributeAsKey()`` method:: + + $node + ->fixXmlConfig('connection') + ->children() + ->arrayNode('connections') + ->useAttributeAsKey('name') + ->prototype('array') + ->children() + ->scalarNode('table')->end() + ->scalarNode('user')->end() + ->scalarNode('password')->end() + ->end() + ->end() + ->end() + ->end() + ; + +The argument of this method (``name`` in the example above) defines the name of +the attribute added to each XML node to differentiate them. Now you can use the +same YAML configuration shown before or the following XML configuration: + +.. code-block:: xml + + + + +In both cases, the processed configuration maintains the ``sf_connection`` and +``default`` keys:: + + Array( + [sf_connection] => Array( + [table] => 'symfony' + [user] => 'root' + [password] => null + ) + [default] => Array( + [table] => 'foo' + [user] => 'root' + [password] => 'pa$$' + ) + ) + +Default and Required Values --------------------------- For all node types, it is possible to define default values and replacement @@ -246,7 +376,8 @@ has a certain value: ``default*()`` (``null``, ``true``, ``false``), shortcut for ``defaultValue()`` ``treat*Like()`` - (``null``, ``true``, ``false``), provide a replacement value in case the value is ``*.`` + (``null``, ``true``, ``false``), provide a replacement value in case + the value is ``*.`` .. code-block:: php @@ -286,10 +417,33 @@ Documenting the Option All options can be documented using the :method:`Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::info` -method. +method:: + + $rootNode + ->children() + ->integerNode('entries_per_page') + ->info('This value is only used for the search results page.') + ->defaultValue(25) + ->end() + ->end() + ; + +The info will be printed as a comment when dumping the configuration tree +with the ``config:dump-reference`` command. + +In YAML you may have: + +.. code-block:: yaml + + # This value is only used for the search results page. + entries_per_page: 25 -The info will be printed as a comment when dumping the configuration tree with -the ``config:dump`` command. +and in XML: + +.. code-block:: xml + + + .. versionadded:: 2.6 Since Symfony 2.6, the info will also be added to the exception message @@ -300,8 +454,10 @@ Optional Sections If you have entire sections which are optional and can be enabled/disabled, you can take advantage of the shortcut -:method:`Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition::canBeEnabled` and -:method:`Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition::canBeDisabled` methods:: +:method:`Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition::canBeEnabled` +and +:method:`Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition::canBeDisabled` +methods:: $arrayNode ->canBeEnabled() @@ -318,7 +474,7 @@ you can take advantage of the shortcut ->defaultFalse() ; -The ``canBeDisabled`` method looks about the same except that the section +The ``canBeDisabled()`` method looks about the same except that the section would be enabled by default. Merging Options @@ -327,21 +483,22 @@ Merging Options Extra options concerning the merge process may be provided. For arrays: ``performNoDeepMerging()`` - When the value is also defined in a second configuration array, don’t + When the value is also defined in a second configuration array, don't try to merge an array, but overwrite it entirely For all nodes: ``cannotBeOverwritten()`` - don’t let other configuration arrays overwrite an existing value for this node + don't let other configuration arrays overwrite an existing value for + this node Appending Sections ------------------ If you have a complex configuration to validate then the tree can grow to -be large and you may want to split it up into sections. You can do this by -making a section a separate node and then appending it into the main tree -with ``append()``:: +be large and you may want to split it up into sections. You can do this +by making a section a separate node and then appending it into the main +tree with ``append()``:: public function getConfigTreeBuilder() { @@ -375,8 +532,8 @@ with ``append()``:: public function addParametersNode() { - $builder = new TreeBuilder(); - $node = $builder->root('parameters'); + $treeBuilder = new TreeBuilder(); + $node = $treeBuilder->root('parameters'); $node ->isRequired() @@ -395,6 +552,47 @@ with ``append()``:: This is also useful to help you avoid repeating yourself if you have sections of the config that are repeated in different places. +The example results in the following: + +.. configuration-block:: + + .. code-block:: yaml + + database: + connection: + driver: ~ # Required + host: localhost + username: ~ + password: ~ + memory: false + parameters: # Required + + # Prototype + name: + value: ~ # Required + + .. code-block:: xml + + + + + + + + + + + + .. _component-config-normalization: Normalization @@ -405,9 +603,9 @@ and finally the tree is used to validate the resulting array. The normalization process is used to remove some of the differences that result from different configuration formats, mainly the differences between YAML and XML. -The separator used in keys is typically ``_`` in YAML and ``-`` in XML. For -example, ``auto_connect`` in YAML and ``auto-connect`` in XML. -The normalization would make both of these ``auto_connect``. +The separator used in keys is typically ``_`` in YAML and ``-`` in XML. +For example, ``auto_connect`` in YAML and ``auto-connect`` in XML. The +normalization would make both of these ``auto_connect``. .. caution:: @@ -432,8 +630,8 @@ and in XML: This difference can be removed in normalization by pluralizing the key used -in XML. You can specify that you want a key to be pluralized in this way with -``fixXmlConfig()``:: +in XML. You can specify that you want a key to be pluralized in this way +with ``fixXmlConfig()``:: $rootNode ->fixXmlConfig('extension') @@ -456,7 +654,7 @@ a second argument:: ->end() ; -As well as fixing this, ``fixXmlConfig`` ensures that single XML elements +As well as fixing this, ``fixXmlConfig()`` ensures that single XML elements are still turned into an array. So you may have: .. code-block:: xml @@ -472,12 +670,12 @@ and sometimes only: By default ``connection`` would be an array in the first case and a string in the second making it difficult to validate. You can ensure it is always -an array with ``fixXmlConfig``. +an array with ``fixXmlConfig()``. You can further control the normalization process if you need to. For example, -you may want to allow a string to be set and used as a particular key or several -keys to be set explicitly. So that, if everything apart from ``name`` is optional -in this config: +you may want to allow a string to be set and used as a particular key or +several keys to be set explicitly. So that, if everything apart from ``name`` +is optional in this config: .. code-block:: yaml @@ -526,8 +724,8 @@ The builder is used for adding advanced validation rules to node definitions, li ->scalarNode('driver') ->isRequired() ->validate() - ->ifNotInArray(array('mysql', 'sqlite', 'mssql')) - ->thenInvalid('Invalid database driver "%s"') + ->ifNotInArray(array('mysql', 'sqlite', 'mssql')) + ->thenInvalid('Invalid database driver %s') ->end() ->end() ->end() @@ -535,8 +733,8 @@ The builder is used for adding advanced validation rules to node definitions, li ->end() ; -A validation rule always has an "if" part. You can specify this part in the -following ways: +A validation rule always has an "if" part. You can specify this part in +the following ways: - ``ifTrue()`` - ``ifString()`` @@ -560,25 +758,30 @@ of the node's original value. Processing Configuration Values ------------------------------- -The :class:`Symfony\\Component\\Config\\Definition\\Processor` uses the tree -as it was built using the :class:`Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder` -to process multiple arrays of configuration values that should be merged. -If any value is not of the expected type, is mandatory and yet undefined, -or could not be validated in some other way, an exception will be thrown. +The :class:`Symfony\\Component\\Config\\Definition\\Processor` uses the +tree as it was built using the +:class:`Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder` to +process multiple arrays of configuration values that should be merged. If +any value is not of the expected type, is mandatory and yet undefined, or +could not be validated in some other way, an exception will be thrown. Otherwise the result is a clean array of configuration values:: use Symfony\Component\Yaml\Yaml; use Symfony\Component\Config\Definition\Processor; use Acme\DatabaseConfiguration; - $config1 = Yaml::parse(file_get_contents(__DIR__.'/src/Matthias/config/config.yml')); - $config2 = Yaml::parse(file_get_contents(__DIR__.'/src/Matthias/config/config_extra.yml')); + $config = Yaml::parse( + file_get_contents(__DIR__.'/src/Matthias/config/config.yml') + ); + $extraConfig = Yaml::parse( + file_get_contents(__DIR__.'/src/Matthias/config/config_extra.yml') + ); - $configs = array($config1, $config2); + $configs = array($config, $extraConfig); $processor = new Processor(); - $configuration = new DatabaseConfiguration(); + $databaseConfiguration = new DatabaseConfiguration(); $processedConfiguration = $processor->processConfiguration( - $configuration, + $databaseConfiguration, $configs ); diff --git a/components/config/index.rst b/components/config/index.rst deleted file mode 100644 index 9aebe7a7c85..00000000000 --- a/components/config/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Config -====== - -.. toctree:: - :maxdepth: 2 - - introduction - resources - caching - definition diff --git a/components/config/resources.rst b/components/config/resources.rst index 1a5fba23c2b..1f56aa69ada 100644 --- a/components/config/resources.rst +++ b/components/config/resources.rst @@ -14,30 +14,31 @@ Loading Resources Locating Resources ------------------ -Loading the configuration normally starts with a search for resources – in -most cases: files. This can be done with the :class:`Symfony\\Component\\Config\\FileLocator`:: +Loading the configuration normally starts with a search for resources, mostly +files. This can be done with the :class:`Symfony\\Component\\Config\\FileLocator`:: use Symfony\Component\Config\FileLocator; $configDirectories = array(__DIR__.'/app/config'); - $locator = new FileLocator($configDirectories); - $yamlUserFiles = $locator->locate('users.yml', null, false); + $fileLocator = new FileLocator($configDirectories); + $yamlUserFiles = $fileLocator->locate('users.yml', null, false); -The locator receives a collection of locations where it should look for files. -The first argument of ``locate()`` is the name of the file to look for. The -second argument may be the current path and when supplied, the locator will -look in this directory first. The third argument indicates whether or not the -locator should return the first file it has found, or an array containing -all matches. +The locator receives a collection of locations where it should look for +files. The first argument of ``locate()`` is the name of the file to look +for. The second argument may be the current path and when supplied, the +locator will look in this directory first. The third argument indicates +whether or not the locator should return the first file it has found or +an array containing all matches. Resource Loaders ---------------- -For each type of resource (YAML, XML, annotation, etc.) a loader must be defined. -Each loader should implement :class:`Symfony\\Component\\Config\\Loader\\LoaderInterface` -or extend the abstract :class:`Symfony\\Component\\Config\\Loader\\FileLoader` -class, which allows for recursively importing other resources:: +For each type of resource (YAML, XML, annotation, etc.) a loader must be +defined. Each loader should implement +:class:`Symfony\\Component\\Config\\Loader\\LoaderInterface` or extend the +abstract :class:`Symfony\\Component\\Config\\Loader\\FileLoader` class, +which allows for recursively importing other resources:: use Symfony\Component\Config\Loader\FileLoader; use Symfony\Component\Yaml\Yaml; @@ -64,24 +65,26 @@ class, which allows for recursively importing other resources:: } } -Finding the right Loader +Finding the Right Loader ------------------------ -The :class:`Symfony\\Component\\Config\\Loader\\LoaderResolver` receives as -its first constructor argument a collection of loaders. When a resource (for -instance an XML file) should be loaded, it loops through this collection -of loaders and returns the loader which supports this particular resource type. +The :class:`Symfony\\Component\\Config\\Loader\\LoaderResolver` receives +as its first constructor argument a collection of loaders. When a resource +(for instance an XML file) should be loaded, it loops through this collection +of loaders and returns the loader which supports this particular resource +type. -The :class:`Symfony\\Component\\Config\\Loader\\DelegatingLoader` makes use -of the :class:`Symfony\\Component\\Config\\Loader\\LoaderResolver`. When -it is asked to load a resource, it delegates this question to the -:class:`Symfony\\Component\\Config\\Loader\\LoaderResolver`. In case the resolver -has found a suitable loader, this loader will be asked to load the resource:: +The :class:`Symfony\\Component\\Config\\Loader\\DelegatingLoader` makes +use of the :class:`Symfony\\Component\\Config\\Loader\\LoaderResolver`. +When it is asked to load a resource, it delegates this question to the +:class:`Symfony\\Component\\Config\\Loader\\LoaderResolver`. In case the +resolver has found a suitable loader, this loader will be asked to load +the resource:: use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Loader\DelegatingLoader; - $loaderResolver = new LoaderResolver(array(new YamlUserLoader($locator))); + $loaderResolver = new LoaderResolver(array(new YamlUserLoader($fileLocator))); $delegatingLoader = new DelegatingLoader($loaderResolver); $delegatingLoader->load(__DIR__.'/users.yml'); diff --git a/components/console.rst b/components/console.rst new file mode 100644 index 00000000000..27f670055f6 --- /dev/null +++ b/components/console.rst @@ -0,0 +1,65 @@ +.. index:: + single: Console; CLI + single: Components; Console + +The Console Component +===================== + + The Console component eases the creation of beautiful and testable command + line interfaces. + +The Console component allows you to create command-line commands. Your console +commands can be used for any recurring task, such as cronjobs, imports, or +other batch jobs. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/console + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +Creating a Console Application +------------------------------ + +First, you need to create a PHP script to define the console application:: + + #!/usr/bin/env php + run(); + +Then, you can register the commands using +:method:`Symfony\\Component\\Console\\Application::add`:: + + // ... + $application->add(new GenerateAdminCommand()); + +See the :doc:`/console` article for information about how to create commands. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /console + /components/console/* + /components/console/helpers/index + /console/* + +.. _Packagist: https://packagist.org/packages/symfony/console diff --git a/components/console/changing_default_command.rst b/components/console/changing_default_command.rst index adbf31a4ae3..29534ba7d23 100644 --- a/components/console/changing_default_command.rst +++ b/components/console/changing_default_command.rst @@ -6,7 +6,7 @@ Changing the Default Command The Console component will always run the ``ListCommand`` when no command name is passed. In order to change the default command you just need to pass the command -name to the ``setDefaultCommand`` method:: +name to the ``setDefaultCommand()`` method:: namespace Acme\Console\Command; @@ -28,7 +28,7 @@ name to the ``setDefaultCommand`` method:: } } -Executing the application and changing the default Command:: +Executing the application and changing the default command:: // application.php @@ -43,7 +43,7 @@ Executing the application and changing the default Command:: Test the new default console command by running the following: -.. code-block:: bash +.. code-block:: terminal $ php application.php @@ -53,9 +53,10 @@ This will print the following to the command line: Hello World -.. tip:: +.. caution:: - This feature has a limitation: you cannot use it with any Command arguments. + This feature has a limitation: you cannot pass any argument or option to + the default command because they are ignored. Learn More! ----------- diff --git a/components/console/console_arguments.rst b/components/console/console_arguments.rst index 894680b684a..fd3474c8ca2 100644 --- a/components/console/console_arguments.rst +++ b/components/console/console_arguments.rst @@ -49,18 +49,18 @@ is required. It can be separated from the option name either by spaces or except that it doesn't require a value. Have a look at the following table to get an overview of the possible ways to pass options: -===================== ========= =========== ============ -Input ``foo`` ``bar`` ``cat`` -===================== ========= =========== ============ -``--bar=Hello`` ``false`` ``"Hello"`` ``null`` -``--bar Hello`` ``false`` ``"Hello"`` ``null`` -``-b=Hello`` ``false`` ``"Hello"`` ``null`` -``-b Hello`` ``false`` ``"Hello"`` ``null`` -``-bHello`` ``false`` ``"Hello"`` ``null`` -``-fcWorld -b Hello`` ``true`` ``"Hello"`` ``"World"`` -``-cfWorld -b Hello`` ``false`` ``"Hello"`` ``"fWorld"`` -``-cbWorld`` ``false`` ``null`` ``"bWorld"`` -===================== ========= =========== ============ +===================== ========= ============ ============ +Input ``foo`` ``bar`` ``cat`` +===================== ========= ============ ============ +``--bar=Hello`` ``false`` ``"Hello"`` ``null`` +``--bar Hello`` ``false`` ``"Hello"`` ``null`` +``-b=Hello`` ``false`` ``"=Hello"`` ``null`` +``-b Hello`` ``false`` ``"Hello"`` ``null`` +``-bHello`` ``false`` ``"Hello"`` ``null`` +``-fcWorld -b Hello`` ``true`` ``"Hello"`` ``"World"`` +``-cfWorld -b Hello`` ``false`` ``"Hello"`` ``"fWorld"`` +``-cbWorld`` ``false`` ``null`` ``"bWorld"`` +===================== ========= ============ ============ Things get a little bit more tricky when the command also accepts an optional argument:: @@ -77,15 +77,15 @@ arguments. Have a look at the fifth example in the following table where it is used to tell the command that ``World`` is the value for ``arg`` and not the value of the optional ``cat`` option: -============================== ================= =========== =========== -Input ``bar`` ``cat`` ``arg`` -============================== ================= =========== =========== -``--bar Hello`` ``"Hello"`` ``null`` ``null`` -``--bar Hello World`` ``"Hello"`` ``null`` ``"World"`` -``--bar "Hello World"`` ``"Hello World"`` ``null`` ``null`` -``--bar Hello --cat World`` ``"Hello"`` ``"World"`` ``null`` -``--bar Hello --cat -- World`` ``"Hello"`` ``null`` ``"World"`` -``-b Hello -c World`` ``"Hello"`` ``"World"`` ``null`` -============================== ================= =========== =========== +============================== ================= =========== =========== +Input ``bar`` ``cat`` ``arg`` +============================== ================= =========== =========== +``--bar Hello`` ``"Hello"`` ``null`` ``null`` +``--bar Hello World`` ``"Hello"`` ``null`` ``"World"`` +``--bar "Hello World"`` ``"Hello World"`` ``null`` ``null`` +``--bar Hello --cat World`` ``"Hello"`` ``"World"`` ``null`` +``--bar Hello --cat -- World`` ``"Hello"`` ``null`` ``"World"`` +``-b Hello -c World`` ``"Hello"`` ``"World"`` ``null`` +============================== ================= =========== =========== .. _docopt: http://docopt.org/ diff --git a/components/console/events.rst b/components/console/events.rst index e5b51c910a1..da49c5245a3 100644 --- a/components/console/events.rst +++ b/components/console/events.rst @@ -40,19 +40,19 @@ dispatched. Listeners receive a use Symfony\Component\Console\ConsoleEvents; $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event) { - // get the input instance + // gets the input instance $input = $event->getInput(); - // get the output instance + // gets the output instance $output = $event->getOutput(); - // get the command to be executed + // gets the command to be executed $command = $event->getCommand(); - // write something about the command + // writes something about the command $output->writeln(sprintf('Before running command %s', $command->getName())); - // get the application + // gets the application $application = $command->getApplication(); }); @@ -74,12 +74,12 @@ C/C++ standard.:: use Symfony\Component\Console\ConsoleEvents; $dispatcher->addListener(ConsoleEvents::COMMAND, function (ConsoleCommandEvent $event) { - // get the command to be executed + // gets the command to be executed $command = $event->getCommand(); // ... check if the command can be executed - // disable the command, this will result in the command being skipped + // disables the command, this will result in the command being skipped // and code 113 being returned from the Application $event->disableCommand(); @@ -89,6 +89,36 @@ C/C++ standard.:: } }); +The ``ConsoleEvents::EXCEPTION`` Event +-------------------------------------- + +**Typical Purposes**: Handle exceptions thrown during the execution of a +command. + +Whenever an exception is thrown by a command, the ``ConsoleEvents::EXCEPTION`` +event is dispatched. A listener can wrap or change the exception or do +anything useful before the exception is thrown by the application. + +Listeners receive a +:class:`Symfony\\Component\\Console\\Event\\ConsoleExceptionEvent` event:: + + use Symfony\Component\Console\Event\ConsoleExceptionEvent; + use Symfony\Component\Console\ConsoleEvents; + + $dispatcher->addListener(ConsoleEvents::EXCEPTION, function (ConsoleExceptionEvent $event) { + $output = $event->getOutput(); + + $command = $event->getCommand(); + + $output->writeln(sprintf('Oops, exception thrown while running command %s', $command->getName())); + + // gets the current exit code (the exception code or the exit code set by a ConsoleEvents::TERMINATE event) + $exitCode = $event->getExitCode(); + + // changes the exception to another one + $event->setException(new \LogicException('Caught exception', $exitCode, $event->getException())); + }); + The ``ConsoleEvents::TERMINATE`` Event -------------------------------------- @@ -108,53 +138,23 @@ Listeners receive a use Symfony\Component\Console\ConsoleEvents; $dispatcher->addListener(ConsoleEvents::TERMINATE, function (ConsoleTerminateEvent $event) { - // get the output + // gets the output $output = $event->getOutput(); - // get the command that has been executed + // gets the command that has been executed $command = $event->getCommand(); - // display something + // displays the given content $output->writeln(sprintf('After running command %s', $command->getName())); - // change the exit code + // changes the exit code $event->setExitCode(128); }); .. tip:: This event is also dispatched when an exception is thrown by the command. - It is then dispatched just before the ``ConsoleEvents::EXCEPTION`` event. + It is then dispatched just after the ``ConsoleEvents::EXCEPTION`` event. The exit code received in this case is the exception code. -The ``ConsoleEvents::EXCEPTION`` Event --------------------------------------- - -**Typical Purposes**: Handle exceptions thrown during the execution of a -command. - -Whenever an exception is thrown by a command, the ``ConsoleEvents::EXCEPTION`` -event is dispatched. A listener can wrap or change the exception or do -anything useful before the exception is thrown by the application. - -Listeners receive a -:class:`Symfony\\Component\\Console\\Event\\ConsoleExceptionEvent` event:: - - use Symfony\Component\Console\Event\ConsoleExceptionEvent; - use Symfony\Component\Console\ConsoleEvents; - - $dispatcher->addListener(ConsoleEvents::EXCEPTION, function (ConsoleExceptionEvent $event) { - $output = $event->getOutput(); - - $command = $event->getCommand(); - - $output->writeln(sprintf('Oops, exception thrown while running command %s', $command->getName())); - - // get the current exit code (the exception code or the exit code set by a ConsoleEvents::TERMINATE event) - $exitCode = $event->getExitCode(); - - // change the exception to another one - $event->setException(new \LogicException('Caught exception', $exitCode, $event->getException())); - }); - .. _`reserved exit codes`: http://www.tldp.org/LDP/abs/html/exitcodes.html diff --git a/components/console/helpers/debug_formatter.rst b/components/console/helpers/debug_formatter.rst index 885c89ab69c..7eacce3464e 100644 --- a/components/console/helpers/debug_formatter.rst +++ b/components/console/helpers/debug_formatter.rst @@ -13,7 +13,7 @@ instance a process or HTTP request. For example, if you used it to output the results of running ``ls -la`` on a UNIX system, it might output something like this: -.. image:: /images/components/console/debug_formatter.png +.. image:: /_images/components/console/debug_formatter.png :align: center Using the debug_formatter @@ -36,7 +36,7 @@ multiple programs at the same time. When using the .. tip:: This information is often too verbose to be shown by default. You can use - :ref:`verbosity levels ` to only show it when in + :doc:`verbosity levels ` to only show it when in debugging mode (``-vvv``). Starting a Program @@ -117,7 +117,7 @@ Stopping a Program ------------------ When a program is stopped, you can use -:method:`Symfony\\Component\\Console\\Helper\\DebugFormatterHelper::run` to +:method:`Symfony\\Component\\Console\\Helper\\DebugFormatterHelper::stop` to notify this to the users:: // ... @@ -125,7 +125,7 @@ notify this to the users:: $debugFormatter->stop( spl_object_hash($process), 'Some command description', - $process->isSuccessfull() + $process->isSuccessful() ) ); diff --git a/components/console/helpers/dialoghelper.rst b/components/console/helpers/dialoghelper.rst index 09af2971d65..cccf89d42cd 100644 --- a/components/console/helpers/dialoghelper.rst +++ b/components/console/helpers/dialoghelper.rst @@ -52,7 +52,7 @@ You can also ask question with more than a simple yes/no answer. For instance, if you want to know a bundle name, you can add this to your command:: // ... - $bundle = $dialog->ask( + $bundleName = $dialog->ask( $output, 'Please enter the name of the bundle', 'AcmeDemoBundle' @@ -71,7 +71,7 @@ will be autocompleted as the user types:: $dialog = $this->getHelper('dialog'); $bundleNames = array('AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle'); - $name = $dialog->ask( + $bundleName = $dialog->ask( $output, 'Please enter the name of a bundle', 'FooBundle', @@ -109,7 +109,7 @@ be suffixed with ``Bundle``. You can validate that by using the method:: // ... - $bundle = $dialog->askAndValidate( + $bundleName = $dialog->askAndValidate( $output, 'Please enter the name of the bundle', function ($answer) { @@ -125,7 +125,7 @@ method:: 'AcmeDemoBundle' ); -This methods has 2 new arguments, the full signature is:: +This method has 2 new arguments, the full signature is:: askAndValidate( OutputInterface $output, @@ -142,11 +142,15 @@ in the console, so it is a good practice to put some useful information in it. T function should also return the value of the user's input if the validation was successful. You can set the max number of times to ask in the ``$attempts`` argument. -If you reach this max number it will use the default value. Using ``false`` means the amount of attempts is infinite. The user will be asked as long as they provide an invalid answer and will only be able to proceed if their input is valid. +Each time the user is asked the question, the default one is used if no answer +is supplied (and validated with the ``$validator`` callback). If the last +attempt is reached, the application will throw an exception and ends its +execution. + Validating a Hidden Response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -156,7 +160,7 @@ You can also ask and validate a hidden response:: $validator = function ($value) { if ('' === trim($value)) { - throw new \Exception('The password can not be empty'); + throw new \Exception('The password cannot be empty'); } return $value; @@ -177,8 +181,8 @@ Let the User Choose from a List of Answers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you have a predefined set of answers the user can choose from, you -could use the ``ask`` method described above or, to make sure the user -provided a correct answer, the ``askAndValidate`` method. Both have +could use the ``ask()`` method described above or, to make sure the user +provided a correct answer, the ``askAndValidate()`` method. Both have the disadvantage that you need to handle incorrect values yourself. Instead, you can use the @@ -288,4 +292,4 @@ input stream. .. seealso:: You find more information about testing commands in the console component - docs about :ref:`testing console commands `. + docs about :ref:`testing console commands `. diff --git a/components/console/helpers/formatterhelper.rst b/components/console/helpers/formatterhelper.rst index 12386d04c3f..18982bb0fb1 100644 --- a/components/console/helpers/formatterhelper.rst +++ b/components/console/helpers/formatterhelper.rst @@ -4,9 +4,9 @@ Formatter Helper ================ -The Formatter helpers provides functions to format the output with colors. +The Formatter helper provides functions to format the output with colors. You can do more advanced things with this helper than you can in -:ref:`components-console-coloring`. +:doc:`/console/coloring`. The :class:`Symfony\\Component\\Console\\Helper\\FormatterHelper` is included in the default helper set, which you can get by calling @@ -62,4 +62,4 @@ messages and 2 spaces on the left and right). The exact "style" you use in the block is up to you. In this case, you're using the pre-defined ``error`` style, but there are other styles, or you can create -your own. See :ref:`components-console-coloring`. +your own. See :doc:`/console/coloring`. diff --git a/components/console/helpers/processhelper.rst b/components/console/helpers/processhelper.rst index 5494ca0137f..7ba70340b4e 100644 --- a/components/console/helpers/processhelper.rst +++ b/components/console/helpers/processhelper.rst @@ -23,15 +23,15 @@ a very verbose verbosity (e.g. -vv):: will result in this output: -.. image:: /images/components/console/process-helper-verbose.png +.. image:: /_images/components/console/process-helper-verbose.png It will result in more detailed output with debug verbosity (e.g. ``-vvv``): -.. image:: /images/components/console/process-helper-debug.png +.. image:: /_images/components/console/process-helper-debug.png In case the process fails, debugging is easier: -.. image:: /images/components/console/process-helper-error-debug.png +.. image:: /_images/components/console/process-helper-error-debug.png Arguments --------- diff --git a/components/console/helpers/progressbar.rst b/components/console/helpers/progressbar.rst index 0e7900b4df0..609ce77e08c 100644 --- a/components/console/helpers/progressbar.rst +++ b/components/console/helpers/progressbar.rst @@ -7,7 +7,7 @@ Progress Bar When executing longer-running commands, it may be helpful to show progress information, which updates as your command runs: -.. image:: /images/components/console/progressbar.gif +.. image:: /_images/components/console/progressbar.gif To display progress details, use the :class:`Symfony\\Component\\Console\\Helper\\ProgressBar`, pass it a total @@ -15,25 +15,25 @@ number of units, and advance the progress as the command executes:: use Symfony\Component\Console\Helper\ProgressBar; - // create a new progress bar (50 units) - $progress = new ProgressBar($output, 50); + // creates a new progress bar (50 units) + $progressBar = new ProgressBar($output, 50); - // start and displays the progress bar - $progress->start(); + // starts and displays the progress bar + $progressBar->start(); $i = 0; while ($i++ < 50) { // ... do some work - // advance the progress bar 1 unit - $progress->advance(); + // advances the progress bar 1 unit + $progressBar->advance(); // you can also advance the progress bar by more than 1 unit - // $progress->advance(3); + // $progressBar->advance(3); } - // ensure that the progress bar is at 100% - $progress->finish(); + // ensures that the progress bar is at 100% + $progressBar->finish(); Instead of advancing the bar by a number of steps (with the :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::advance` method), @@ -48,7 +48,8 @@ you can also set the current progress by calling the Prior to version 2.6, the progress bar only works if your platform supports ANSI codes; on other platforms, no output is generated. -.. versionadded:: 2.6 +.. tip:: + If your platform doesn't support ANSI codes, updates to the progress bar are added as new lines. To prevent the output from being flooded, adjust the @@ -56,11 +57,14 @@ you can also set the current progress by calling the accordingly. By default, when using a ``max``, the redraw frequency is set to *10%* of your ``max``. + .. versionadded:: 2.6 + The ``setRedrawFrequency()`` method was introduced in Symfony 2.6. + If you don't know the number of steps in advance, just omit the steps argument when creating the :class:`Symfony\\Component\\Console\\Helper\\ProgressBar` instance:: - $progress = new ProgressBar($output); + $progressBar = new ProgressBar($output); The progress will then be displayed as a throbber: @@ -127,7 +131,7 @@ level of verbosity of the ``OutputInterface`` instance: Instead of relying on the verbosity mode of the current command, you can also force a format via ``setFormat()``:: - $bar->setFormat('verbose'); + $progressBar->setFormat('verbose'); The built-in formats are the following: @@ -149,7 +153,7 @@ Custom Formats Instead of using the built-in formats, you can also set your own:: - $bar->setFormat('%bar%'); + $progressBar->setFormat('%bar%'); This sets the format to only display the progress bar itself: @@ -171,38 +175,24 @@ current progress of the bar. Here is a list of the built-in placeholders: * ``remaining``: The remaining time to complete the task (not available if no max is defined); * ``estimated``: The estimated time to complete the task (not available if no max is defined); * ``memory``: The current memory usage; -* ``message``: The current message attached to the progress bar. +* ``message``: used to display arbitrary messages in the progress bar (as explained later). For instance, here is how you could set the format to be the same as the ``debug`` one:: - $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'); + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%'); Notice the ``:6s`` part added to some placeholders? That's how you can tweak the appearance of the bar (formatting and alignment). The part after the colon (``:``) is used to set the ``sprintf`` format of the string. -The ``message`` placeholder is a bit special as you must set the value -yourself:: - - $bar->setMessage('Task starts'); - $bar->start(); - - $bar->setMessage('Task in progress...'); - $bar->advance(); - - // ... - - $bar->setMessage('Task is finished'); - $bar->finish(); - Instead of setting the format for a given instance of a progress bar, you can also define global formats:: ProgressBar::setFormatDefinition('minimal', 'Progress: %percent%%'); - $bar = new ProgressBar($output, 3); - $bar->setFormat('minimal'); + $progressBar = new ProgressBar($output, 3); + $progressBar->setFormat('minimal'); This code defines a new ``minimal`` format that you can then use for your progress bars: @@ -226,8 +216,8 @@ variant:: ProgressBar::setFormatDefinition('minimal', '%percent%% %remaining%'); ProgressBar::setFormatDefinition('minimal_nomax', '%percent%%'); - $bar = new ProgressBar($output); - $bar->setFormat('minimal'); + $progressBar = new ProgressBar($output); + $progressBar->setFormat('minimal'); When displaying the progress bar, the format will automatically be set to ``minimal_nomax`` if the bar does not have a maximum number of steps like in @@ -256,16 +246,16 @@ Amongst the placeholders, ``bar`` is a bit special as all the characters used to display it can be customized:: // the finished part of the bar - $progress->setBarCharacter('='); + $progressBar->setBarCharacter('='); // the unfinished part of the bar - $progress->setEmptyBarCharacter(' '); + $progressBar->setEmptyBarCharacter(' '); // the progress character - $progress->setProgressCharacter('|'); + $progressBar->setProgressCharacter('|'); // the bar width - $progress->setBarWidth(50); + $progressBar->setBarWidth(50); .. caution:: @@ -275,17 +265,17 @@ to display it can be customized:: :method:`Symfony\\Component\\Console\\Helper\\ProgressBar::setRedrawFrequency`, so it updates on only some iterations:: - $progress = new ProgressBar($output, 50000); - $progress->start(); + $progressBar = new ProgressBar($output, 50000); + $progressBar->start(); // update every 100 iterations - $progress->setRedrawFrequency(100); + $progressBar->setRedrawFrequency(100); $i = 0; while ($i++ < 50000) { // ... do some work - $progress->advance(); + $progressBar->advance(); } Custom Placeholders @@ -298,8 +288,8 @@ that displays the number of remaining steps:: ProgressBar::setPlaceholderFormatterDefinition( 'remaining_steps', - function (ProgressBar $bar, OutputInterface $output) { - return $bar->getMaxSteps() - $bar->getProgress(); + function (ProgressBar $progressBar, OutputInterface $output) { + return $progressBar->getMaxSteps() - $progressBar->getProgress(); } ); @@ -309,25 +299,43 @@ that displays the number of remaining steps:: Custom Messages ~~~~~~~~~~~~~~~ -The ``%message%`` placeholder allows you to specify a custom message to be -displayed with the progress bar. But if you need more than one, just define -your own:: +Progress bars define a placeholder called ``message`` to display arbitrary +messages. However, none of the built-in formats include that placeholder, so +before displaying these messages, you must define your own custom format:: - $bar->setMessage('Task starts'); - $bar->setMessage('', 'filename'); - $bar->start(); + ProgressBar::setFormatDefinition('custom', ' %current%/%max% -- %message%'); - $bar->setMessage('Task is in progress...'); - while ($file = array_pop($files)) { - $bar->setMessage($filename, 'filename'); - $bar->advance(); - } + $progressBar = new ProgressBar($output, 100); + $progressBar->setFormat('custom'); + +Now, use the ``setMessage()`` method to set the value of the ``%message%`` +placeholder before displaying the progress bar:: + + // ... + $progressBar->setMessage('Start'); + $progressBar->start(); + // 0/100 -- Start + + $progressBar->advance(); + $progressBar->setMessage('Task is in progress...'); + // 1/100 -- Task is in progress... - $bar->setMessage('Task is finished'); - $bar->setMessage('', 'filename'); - $bar->finish(); +Messages can be combined with custom placeholders too. In this example, the +progress bar uses the ``%message%`` and ``%filename%`` placeholders:: -For the ``filename`` to be part of the progress bar, just add the -``%filename%`` placeholder in your format:: + ProgressBar::setFormatDefinition('custom', ' %current%/%max% -- %message% (%filename%)'); - $bar->setFormat(" %message%\n %current%/%max%\n Working on %filename%"); + $progressBar = new ProgressBar($output, 100); + $progressBar->setFormat('custom'); + +The ``setMessage()`` method accepts a second optional argument to set the value +of the custom placeholders:: + + // ... + // $files = array('client-001/invoices.xml', '...'); + foreach ($files as $filename) { + $progressBar->setMessage('Importing invoices...'); + $progressBar->setMessage($filename, 'filename'); + $progressBar->advance(); + // 2/100 -- Importing invoices... (client-001/invoices.xml) + } diff --git a/components/console/helpers/progresshelper.rst b/components/console/helpers/progresshelper.rst index 7d858d21691..7f0f86a23ac 100644 --- a/components/console/helpers/progresshelper.rst +++ b/components/console/helpers/progresshelper.rst @@ -5,7 +5,7 @@ Progress Helper =============== .. versionadded:: 2.3 - The ``setCurrent`` method was introduced in Symfony 2.3. + The ``setCurrent()`` method was introduced in Symfony 2.3. .. caution:: @@ -17,7 +17,7 @@ Progress Helper When executing longer-running commands, it may be helpful to show progress information, which updates as your command runs: -.. image:: /images/components/console/progress.png +.. image:: /_images/components/console/progress.png To display progress details, use the :class:`Symfony\\Component\\Console\\Helper\\ProgressHelper`, pass it a total number of units, and advance the progress as your command executes:: diff --git a/components/console/helpers/questionhelper.rst b/components/console/helpers/questionhelper.rst index f760096b26f..521b6d8d82d 100644 --- a/components/console/helpers/questionhelper.rst +++ b/components/console/helpers/questionhelper.rst @@ -13,7 +13,7 @@ helper set, which you can get by calling The Question Helper has a single method :method:`Symfony\\Component\\Console\\Command\\Command::ask` that needs an -:class:`Symfony\\Component\\Console\\Output\\InputInterface` instance as the +:class:`Symfony\\Component\\Console\\Input\\InputInterface` instance as the first argument, an :class:`Symfony\\Component\\Console\\Output\\OutputInterface` instance as the second argument and a :class:`Symfony\\Component\\Console\\Question\\Question` as last argument. @@ -24,14 +24,24 @@ Asking the User for Confirmation Suppose you want to confirm an action before actually executing it. Add the following to your command:: - use Symfony\Component\Console\Question\ConfirmationQuestion; // ... + use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Output\OutputInterface; + use Symfony\Component\Console\Question\ConfirmationQuestion; - $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion('Continue with this action?', false); + class YourCommand extends Command + { + // ... + + public function execute(InputInterface $input, OutputInterface $output) + { + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Continue with this action?', false); - if (!$helper->ask($input, $output, $question)) { - return; + if (!$helper->ask($input, $output, $question)) { + return; + } + } } In this case, the user will be asked "Continue with this action?". If the user @@ -66,11 +76,15 @@ You can also ask a question with more than a simple yes/no answer. For instance, if you want to know a bundle name, you can add this to your command:: use Symfony\Component\Console\Question\Question; - // ... - $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); - $bundle = $helper->ask($input, $output, $question); + $bundleName = $helper->ask($input, $output, $question); + } The user will be asked "Please enter the name of the bundle". They can type some name which will be returned by the @@ -86,20 +100,24 @@ which makes sure that the user can only enter a valid string from a predefined list:: use Symfony\Component\Console\Question\ChoiceQuestion; - // ... - $helper = $this->getHelper('question'); - $question = new ChoiceQuestion( - 'Please select your favorite color (defaults to red)', - array('red', 'blue', 'yellow'), - 0 - ); - $question->setErrorMessage('Color %s is invalid.'); + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion( + 'Please select your favorite color (defaults to red)', + array('red', 'blue', 'yellow'), + 0 + ); + $question->setErrorMessage('Color %s is invalid.'); - $color = $helper->ask($input, $output, $question); - $output->writeln('You have just selected: '.$color); + $color = $helper->ask($input, $output, $question); + $output->writeln('You have just selected: '.$color); - // ... do something with the color + // ... do something with the color + } The option which should be selected by default is provided with the third argument of the constructor. The default is ``null``, which means that no @@ -120,18 +138,22 @@ feature using comma separated values. This is disabled by default, to enable this use :method:`Symfony\\Component\\Console\\Question\\ChoiceQuestion::setMultiselect`:: use Symfony\Component\Console\Question\ChoiceQuestion; - // ... - $helper = $this->getHelper('question'); - $question = new ChoiceQuestion( - 'Please select your favorite colors (defaults to red and blue)', - array('red', 'blue', 'yellow'), - '0,1' - ); - $question->setMultiselect(true); + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion( + 'Please select your favorite colors (defaults to red and blue)', + array('red', 'blue', 'yellow'), + '0,1' + ); + $question->setMultiselect(true); - $colors = $helper->ask($input, $output, $question); - $output->writeln('You have just selected: ' . implode(', ', $colors)); + $colors = $helper->ask($input, $output, $question); + $output->writeln('You have just selected: ' . implode(', ', $colors)); + } Now, when the user enters ``1,2``, the result will be: ``You have just selected: blue, yellow``. @@ -146,13 +168,19 @@ You can also specify an array of potential answers for a given question. These will be autocompleted as the user types:: use Symfony\Component\Console\Question\Question; + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); - $bundles = array('AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle'); - $question = new Question('Please enter the name of a bundle', 'FooBundle'); - $question->setAutocompleterValues($bundles); + $bundles = array('AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle'); + $question = new Question('Please enter the name of a bundle', 'FooBundle'); + $question->setAutocompleterValues($bundles); - $name = $helper->ask($input, $output, $question); + $bundleName = $helper->ask($input, $output, $question); + } Hiding the User's Response ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -161,13 +189,19 @@ You can also ask a question and hide the response. This is particularly convenient for passwords:: use Symfony\Component\Console\Question\Question; + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); - $question = new Question('What is the database password?'); - $question->setHidden(true); - $question->setHiddenFallback(false); + $question = new Question('What is the database password?'); + $question->setHidden(true); + $question->setHiddenFallback(false); - $password = $helper->ask($input, $output, $question); + $password = $helper->ask($input, $output, $question); + } .. caution:: @@ -179,6 +213,39 @@ convenient for passwords:: like in the example above. In this case, a ``RuntimeException`` would be thrown. +Normalizing the Answer +---------------------- + +Before validating the answer, you can "normalize" it to fix minor errors or +tweak it as needed. For instance, in a previous example you asked for the bundle +name. In case the user adds white spaces around the name by mistake, you can +trim the name before validating it. To do so, configure a normalizer using the +:method:`Symfony\\Component\\Console\\Question\\Question::setNormalizer` +method:: + + use Symfony\Component\Console\Question\Question; + + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); + + $question = new Question('Please enter the name of the bundle', 'AppBundle'); + $question->setNormalizer(function ($value) { + // $value can be null here + return $value ? trim($value) : ''; + }); + + $bundleName = $helper->ask($input, $output, $question); + } + +.. caution:: + + The normalizer is called first and the returned value is used as the input + of the validator. If the answer is invalid, don't throw exceptions in the + normalizer and let the validator handle those errors. + Validating the Answer --------------------- @@ -189,20 +256,27 @@ be suffixed with ``Bundle``. You can validate that by using the method:: use Symfony\Component\Console\Question\Question; + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); - $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); - $question->setValidator(function ($answer) { - if ('Bundle' !== substr($answer, -6)) { - throw new \RuntimeException( - 'The name of the bundle should be suffixed with \'Bundle\'' - ); - } - return $answer; - }); - $question->setMaxAttempts(2); + $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle'); + $question->setValidator(function ($answer) { + if (!is_string($answer) || 'Bundle' !== substr($answer, -6)) { + throw new \RuntimeException( + 'The name of the bundle should be suffixed with \'Bundle\'' + ); + } + + return $answer; + }); + $question->setMaxAttempts(2); - $name = $helper->ask($input, $output, $question); + $bundleName = $helper->ask($input, $output, $question); + } The ``$validator`` is a callback which handles the validation. It should throw an exception if there is something wrong. The exception message is displayed @@ -222,23 +296,26 @@ Validating a Hidden Response You can also use a validator with a hidden question:: use Symfony\Component\Console\Question\Question; - // ... - $helper = $this->getHelper('question'); - - $question = new Question('Please enter your password'); - $question->setValidator(function ($value) { - if (trim($value) == '') { - throw new \Exception('The password can not be empty'); - } + // ... + public function execute(InputInterface $input, OutputInterface $output) + { + // ... + $helper = $this->getHelper('question'); - return $value; - }); - $question->setHidden(true); - $question->setMaxAttempts(20); + $question = new Question('Please enter your password'); + $question->setValidator(function ($value) { + if (trim($value) == '') { + throw new \Exception('The password cannot be empty'); + } - $password = $helper->ask($input, $output, $question); + return $value; + }); + $question->setHidden(true); + $question->setMaxAttempts(20); + $password = $helper->ask($input, $output, $question); + } Testing a Command that Expects Input ------------------------------------ @@ -257,7 +334,7 @@ from the command line, you need to set the helper input stream:: $commandTester = new CommandTester($command); $helper = $command->getHelper('question'); - $helper->setInputStream($this->getInputStream('Test\\n')); + $helper->setInputStream($this->getInputStream("Test\n")); // Equals to a user inputting "Test" and hitting ENTER // If you need to enter a confirmation, "yes\n" will work @@ -276,6 +353,6 @@ from the command line, you need to set the helper input stream:: } By setting the input stream of the ``QuestionHelper``, you imitate what the -console would do internally with all user input through the cli. This way +console would do internally with all user input through the CLI. This way you can test any user interaction (even complex ones) by passing an appropriate input stream. diff --git a/components/console/helpers/table.rst b/components/console/helpers/table.rst index b4f21eb964c..dca380a4ee3 100644 --- a/components/console/helpers/table.rst +++ b/components/console/helpers/table.rst @@ -109,17 +109,17 @@ If the built-in styles do not fit your need, define your own:: use Symfony\Component\Console\Helper\TableStyle; // by default, this is based on the default style - $style = new TableStyle(); + $tableStyle = new TableStyle(); - // customize the style - $style + // customizes the style + $tableStyle ->setHorizontalBorderChar('|') ->setVerticalBorderChar('-') ->setCrossingChar(' ') ; - // use the style for this table - $table->setStyle($style); + // uses the custom style for this table + $table->setStyle($tableStyle); Here is a full list of things you can customize: @@ -136,10 +136,101 @@ Here is a full list of things you can customize: You can also register a style globally:: - // register the style under the colorful name - Table::setStyleDefinition('colorful', $style); + // registers the style under the colorful name + Table::setStyleDefinition('colorful', $tableStyle); - // use it for a table + // applies the custom style for the given table $table->setStyle('colorful'); This method can also be used to override a built-in style. + +Spanning Multiple Columns and Rows +---------------------------------- + +.. versionadded:: 2.7 + Spanning multiple columns and rows was introduced in Symfony 2.7. + +To make a table cell that spans multiple columns you can use a :class:`Symfony\\Component\\Console\\Helper\\TableCell`:: + + use Symfony\Component\Console\Helper\Table; + use Symfony\Component\Console\Helper\TableSeparator; + use Symfony\Component\Console\Helper\TableCell; + + $table = new Table($output); + $table + ->setHeaders(array('ISBN', 'Title', 'Author')) + ->setRows(array( + array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'), + new TableSeparator(), + array(new TableCell('This value spans 3 columns.', array('colspan' => 3))), + )) + ; + $table->render(); + +This results in: + +.. code-block:: text + + +---------------+---------------+-----------------+ + | ISBN | Title | Author | + +---------------+---------------+-----------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + +---------------+---------------+-----------------+ + | This value spans 3 columns. | + +---------------+---------------+-----------------+ + +.. tip:: + + You can create a multiple-line page title using a header cell that spans + the entire table width:: + + $table->setHeaders(array( + array(new TableCell('Main table title', array('colspan' => 3))), + array('ISBN', 'Title', 'Author'), + )) + // ... + + This generates: + + .. code-block:: text + + +-------+-------+--------+ + | Main table title | + +-------+-------+--------+ + | ISBN | Title | Author | + +-------+-------+--------+ + | ... | + +-------+-------+--------+ + +In a similar way you can span multiple rows:: + + use Symfony\Component\Console\Helper\Table; + use Symfony\Component\Console\Helper\TableCell; + + $table = new Table($output); + $table + ->setHeaders(array('ISBN', 'Title', 'Author')) + ->setRows(array( + array( + '978-0521567817', + 'De Monarchia', + new TableCell("Dante Alighieri\nspans multiple rows", array('rowspan' => 2)), + ), + array('978-0804169127', 'Divine Comedy'), + )) + ; + $table->render(); + +This outputs: + +.. code-block:: text + + +----------------+---------------+---------------------+ + | ISBN | Title | Author | + +----------------+---------------+---------------------+ + | 978-0521567817 | De Monarchia | Dante Alighieri | + | 978-0804169127 | Divine Comedy | spans multiple rows | + +----------------+---------------+---------------------+ + +You can use the ``colspan`` and ``rowspan`` options at the same time which allows +you to create any table layout you may wish. diff --git a/components/console/helpers/tablehelper.rst b/components/console/helpers/tablehelper.rst index 0e508b4d4d6..cc853ebdad2 100644 --- a/components/console/helpers/tablehelper.rst +++ b/components/console/helpers/tablehelper.rst @@ -16,7 +16,7 @@ Table Helper When building a console application it may be useful to display tabular data: -.. image:: /images/components/console/table.png +.. image:: /_images/components/console/table.png To display a table, use the :class:`Symfony\\Component\\Console\\Helper\\TableHelper`, set headers, rows and render:: diff --git a/components/console/index.rst b/components/console/index.rst deleted file mode 100644 index 1ae77d50566..00000000000 --- a/components/console/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Console -======= - -.. toctree:: - :maxdepth: 2 - - introduction - usage - changing_default_command - single_command_tool - console_arguments - events - logger - helpers/index diff --git a/components/console/introduction.rst b/components/console/introduction.rst deleted file mode 100644 index 9c17eb3b479..00000000000 --- a/components/console/introduction.rst +++ /dev/null @@ -1,563 +0,0 @@ -.. index:: - single: Console; CLI - single: Components; Console - -The Console Component -===================== - - The Console component eases the creation of beautiful and testable command - line interfaces. - -The Console component allows you to create command-line commands. Your console -commands can be used for any recurring task, such as cronjobs, imports, or -other batch jobs. - -Installation ------------- - -You can install the component in 2 different ways: - -* :doc:`Install it via Composer ` (``symfony/console`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Console). - -.. include:: /components/require_autoload.rst.inc - -Creating a basic Command ------------------------- - -To make a console command that greets you from the command line, create ``GreetCommand.php`` -and add the following to it:: - - namespace Acme\Console\Command; - - use Symfony\Component\Console\Command\Command; - use Symfony\Component\Console\Input\InputArgument; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Input\InputOption; - use Symfony\Component\Console\Output\OutputInterface; - - class GreetCommand extends Command - { - protected function configure() - { - $this - ->setName('demo:greet') - ->setDescription('Greet someone') - ->addArgument( - 'name', - InputArgument::OPTIONAL, - 'Who do you want to greet?' - ) - ->addOption( - 'yell', - null, - InputOption::VALUE_NONE, - 'If set, the task will yell in uppercase letters' - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $name = $input->getArgument('name'); - if ($name) { - $text = 'Hello '.$name; - } else { - $text = 'Hello'; - } - - if ($input->getOption('yell')) { - $text = strtoupper($text); - } - - $output->writeln($text); - } - } - -You also need to create the file to run at the command line which creates -an ``Application`` and adds commands to it:: - - #!/usr/bin/env php - add(new GreetCommand()); - $application->run(); - -Test the new console command by running the following - -.. code-block:: bash - - $ php application.php demo:greet Fabien - -This will print the following to the command line: - -.. code-block:: text - - Hello Fabien - -You can also use the ``--yell`` option to make everything uppercase: - -.. code-block:: bash - - $ php application.php demo:greet Fabien --yell - -This prints:: - - HELLO FABIEN - -.. _components-console-coloring: - -Coloring the Output -~~~~~~~~~~~~~~~~~~~ - -.. note:: - - By default, the Windows command console doesn't support output coloring. The - Console component disables output coloring for Windows systems, but if your - commands invoke other scripts which emit color sequences, they will be - wrongly displayed as raw escape characters. Install the `ConEmu`_ or `ANSICON`_ - free applications to add coloring support to your Windows command console. - -Whenever you output text, you can surround the text with tags to color its -output. For example:: - - // green text - $output->writeln('foo'); - - // yellow text - $output->writeln('foo'); - - // black text on a cyan background - $output->writeln('foo'); - - // white text on a red background - $output->writeln('foo'); - -The closing tag can be replaced by ````, which revokes all formatting options -established by the last opened tag. - -It is possible to define your own styles using the class -:class:`Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle`:: - - use Symfony\Component\Console\Formatter\OutputFormatterStyle; - - // ... - $style = new OutputFormatterStyle('red', 'yellow', array('bold', 'blink')); - $output->getFormatter()->setStyle('fire', $style); - $output->writeln('foo'); - -Available foreground and background colors are: ``black``, ``red``, ``green``, -``yellow``, ``blue``, ``magenta``, ``cyan`` and ``white``. - -And available options are: ``bold``, ``underscore``, ``blink``, ``reverse`` -(enables the "reverse video" mode where the background and foreground colors -are swapped) and ``conceal`` (sets the foreground color to transparent, making -the typed text invisible - although it can be selected and copied; this option is -commonly used when asking the user to type sensitive information). - -You can also set these colors and options inside the tagname:: - - // green text - $output->writeln('foo'); - - // black text on a cyan background - $output->writeln('foo'); - - // bold text on a yellow background - $output->writeln('foo'); - -.. _verbosity-levels: - -Verbosity Levels -~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.3 - The ``VERBOSITY_VERY_VERBOSE`` and ``VERBOSITY_DEBUG`` constants were introduced - in version 2.3 - -The console has five verbosity levels. These are defined in the -:class:`Symfony\\Component\\Console\\Output\\OutputInterface`: - -=========================================== ================================== ===================== -Value Meaning Console option -=========================================== ================================== ===================== -``OutputInterface::VERBOSITY_QUIET`` Do not output any messages ``-q`` or ``--quiet`` -``OutputInterface::VERBOSITY_NORMAL`` The default verbosity level (none) -``OutputInterface::VERBOSITY_VERBOSE`` Increased verbosity of messages ``-v`` -``OutputInterface::VERBOSITY_VERY_VERBOSE`` Informative non essential messages ``-vv`` -``OutputInterface::VERBOSITY_DEBUG`` Debug messages ``-vvv`` -=========================================== ================================== ===================== - -.. tip:: - - The full exception stacktrace is printed if the ``VERBOSITY_VERBOSE`` - level or above is used. - -It is possible to print a message in a command for only a specific verbosity -level. For example:: - - if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { - $output->writeln(...); - } - -There are also more semantic methods you can use to test for each of the -verbosity levels:: - - if ($output->isQuiet()) { - // ... - } - - if ($output->isVerbose()) { - // ... - } - - if ($output->isVeryVerbose()) { - // ... - } - - if ($output->isDebug()) { - // ... - } - -.. note:: - - These semantic methods are defined in the ``OutputInterface`` starting from - Symfony 3.0. In previous Symfony versions they are defined in the different - implementations of the interface (e.g. :class:`Symfony\\Component\\Console\\Output\\Output`) - in order to keep backwards compatibility. - -When the quiet level is used, all output is suppressed as the default -:method:`Symfony\\Component\\Console\\Output\\Output::write` method returns -without actually printing. - -.. tip:: - - The MonologBridge provides a :class:`Symfony\\Bridge\\Monolog\\Handler\\ConsoleHandler` - class that allows you to display messages on the console. This is cleaner - than wrapping your output calls in conditions. For an example use in - the Symfony Framework, see :doc:`/cookbook/logging/monolog_console`. - -Using Command Arguments ------------------------ - -The most interesting part of the commands are the arguments and options that -you can make available. Arguments are the strings - separated by spaces - that -come after the command name itself. They are ordered, and can be optional -or required. For example, add an optional ``last_name`` argument to the command -and make the ``name`` argument required:: - - $this - // ... - ->addArgument( - 'name', - InputArgument::REQUIRED, - 'Who do you want to greet?' - ) - ->addArgument( - 'last_name', - InputArgument::OPTIONAL, - 'Your last name?' - ); - -You now have access to a ``last_name`` argument in your command:: - - if ($lastName = $input->getArgument('last_name')) { - $text .= ' '.$lastName; - } - -The command can now be used in either of the following ways: - -.. code-block:: bash - - $ php application.php demo:greet Fabien - $ php application.php demo:greet Fabien Potencier - -It is also possible to let an argument take a list of values (imagine you want -to greet all your friends). For this it must be specified at the end of the -argument list:: - - $this - // ... - ->addArgument( - 'names', - InputArgument::IS_ARRAY, - 'Who do you want to greet (separate multiple names with a space)?' - ); - -To use this, just specify as many names as you want: - -.. code-block:: bash - - $ php application.php demo:greet Fabien Ryan Bernhard - -You can access the ``names`` argument as an array:: - - if ($names = $input->getArgument('names')) { - $text .= ' '.implode(', ', $names); - } - -There are three argument variants you can use: - -=========================== =========================================================================================================== -Mode Value -=========================== =========================================================================================================== -``InputArgument::REQUIRED`` The argument is required -``InputArgument::OPTIONAL`` The argument is optional and therefore can be omitted -``InputArgument::IS_ARRAY`` The argument can contain an indefinite number of arguments and must be used at the end of the argument list -=========================== =========================================================================================================== - -You can combine ``IS_ARRAY`` with ``REQUIRED`` and ``OPTIONAL`` like this:: - - $this - // ... - ->addArgument( - 'names', - InputArgument::IS_ARRAY | InputArgument::REQUIRED, - 'Who do you want to greet (separate multiple names with a space)?' - ); - -Using Command Options ---------------------- - -Unlike arguments, options are not ordered (meaning you can specify them in any -order) and are specified with two dashes (e.g. ``--yell`` - you can also -declare a one-letter shortcut that you can call with a single dash like -``-y``). Options are *always* optional, and can be setup to accept a value -(e.g. ``--dir=src``) or simply as a boolean flag without a value (e.g. -``--yell``). - -.. tip:: - - There is nothing forbidding you to create a command with an option that - optionally accepts a value. However, there is no way you can distinguish - when the option was used without a value (``command --yell``) or when it - wasn't used at all (``command``). In both cases, the value retrieved for - the option will be ``null``. - -For example, add a new option to the command that can be used to specify -how many times in a row the message should be printed:: - - $this - // ... - ->addOption( - 'iterations', - null, - InputOption::VALUE_REQUIRED, - 'How many times should the message be printed?', - 1 - ); - -Next, use this in the command to print the message multiple times: - -.. code-block:: php - - for ($i = 0; $i < $input->getOption('iterations'); $i++) { - $output->writeln($text); - } - -Now, when you run the task, you can optionally specify a ``--iterations`` -flag: - -.. code-block:: bash - - $ php application.php demo:greet Fabien - $ php application.php demo:greet Fabien --iterations=5 - -The first example will only print once, since ``iterations`` is empty and -defaults to ``1`` (the last argument of ``addOption``). The second example -will print five times. - -Recall that options don't care about their order. So, either of the following -will work: - -.. code-block:: bash - - $ php application.php demo:greet Fabien --iterations=5 --yell - $ php application.php demo:greet Fabien --yell --iterations=5 - -There are 4 option variants you can use: - -=============================== ===================================================================================== -Option Value -=============================== ===================================================================================== -``InputOption::VALUE_IS_ARRAY`` This option accepts multiple values (e.g. ``--dir=/foo --dir=/bar``) -``InputOption::VALUE_NONE`` Do not accept input for this option (e.g. ``--yell``) -``InputOption::VALUE_REQUIRED`` This value is required (e.g. ``--iterations=5``), the option itself is still optional -``InputOption::VALUE_OPTIONAL`` This option may or may not have a value (e.g. ``--yell`` or ``--yell=loud``) -=============================== ===================================================================================== - -You can combine ``VALUE_IS_ARRAY`` with ``VALUE_REQUIRED`` or ``VALUE_OPTIONAL`` like this: - -.. code-block:: php - - $this - // ... - ->addOption( - 'colors', - null, - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Which colors do you like?', - array('blue', 'red') - ); - -Console Helpers ---------------- - -The console component also contains a set of "helpers" - different small -tools capable of helping you with different tasks: - -* :doc:`/components/console/helpers/questionhelper`: interactively ask the user for information -* :doc:`/components/console/helpers/formatterhelper`: customize the output colorization -* :doc:`/components/console/helpers/progressbar`: shows a progress bar -* :doc:`/components/console/helpers/table`: displays tabular data as a table - -.. _component-console-testing-commands: - -Testing Commands ----------------- - -Symfony provides several tools to help you test your commands. The most -useful one is the :class:`Symfony\\Component\\Console\\Tester\\CommandTester` -class. It uses special input and output classes to ease testing without a real -console:: - - use Acme\Console\Command\GreetCommand; - use Symfony\Component\Console\Application; - use Symfony\Component\Console\Tester\CommandTester; - - class ListCommandTest extends \PHPUnit_Framework_TestCase - { - public function testExecute() - { - $application = new Application(); - $application->add(new GreetCommand()); - - $command = $application->find('demo:greet'); - $commandTester = new CommandTester($command); - $commandTester->execute(array('command' => $command->getName())); - - $this->assertRegExp('/.../', $commandTester->getDisplay()); - - // ... - } - } - -The :method:`Symfony\\Component\\Console\\Tester\\CommandTester::getDisplay` -method returns what would have been displayed during a normal call from the -console. - -You can test sending arguments and options to the command by passing them -as an array to the :method:`Symfony\\Component\\Console\\Tester\\CommandTester::execute` -method:: - - use Acme\Console\Command\GreetCommand; - use Symfony\Component\Console\Application; - use Symfony\Component\Console\Tester\CommandTester; - - class ListCommandTest extends \PHPUnit_Framework_TestCase - { - // ... - - public function testNameIsOutput() - { - $application = new Application(); - $application->add(new GreetCommand()); - - $command = $application->find('demo:greet'); - $commandTester = new CommandTester($command); - $commandTester->execute(array( - 'command' => $command->getName(), - 'name' => 'Fabien', - '--iterations' => 5, - )); - - $this->assertRegExp('/Fabien/', $commandTester->getDisplay()); - } - } - -.. tip:: - - You can also test a whole console application by using - :class:`Symfony\\Component\\Console\\Tester\\ApplicationTester`. - -.. _calling-existing-command: - -Calling an Existing Command ---------------------------- - -If a command depends on another one being run before it, instead of asking the -user to remember the order of execution, you can call it directly yourself. -This is also useful if you want to create a "meta" command that just runs a -bunch of other commands (for instance, all commands that need to be run when -the project's code has changed on the production servers: clearing the cache, -generating Doctrine2 proxies, dumping Assetic assets, ...). - -Calling a command from another one is straightforward:: - - protected function execute(InputInterface $input, OutputInterface $output) - { - $command = $this->getApplication()->find('demo:greet'); - - $arguments = array( - 'command' => 'demo:greet', - 'name' => 'Fabien', - '--yell' => true, - ); - - $greetInput = new ArrayInput($arguments); - $returnCode = $command->run($greetInput, $output); - - // ... - } - -First, you :method:`Symfony\\Component\\Console\\Application::find` the -command you want to execute by passing the command name. Then, you need to create -a new :class:`Symfony\\Component\\Console\\Input\\ArrayInput` with the arguments -and options you want to pass to the command. - -Eventually, calling the ``run()`` method actually executes the command and -returns the returned code from the command (return value from command's -``execute()`` method). - -.. tip:: - - If you want to suppress the output of the executed command, pass a - :class:`Symfony\\Component\\Console\\Output\\NullOutput` as the second - argument to ``$command->execute()``. - -.. caution:: - - Note that all the commands will run in the same process and some of Symfony's - built-in commands may not work well this way. For instance, the ``cache:clear`` - and ``cache:warmup`` commands change some class definitions, so running - something after them is likely to break. - -.. note:: - - Most of the time, calling a command from code that is not executed on the - command line is not a good idea for several reasons. First, the command's - output is optimized for the console. But more important, you can think of - a command as being like a controller; it should use the model to do - something and display feedback to the user. So, instead of calling a - command from the Web, refactor your code and move the logic to a new - class. - -Learn More! ------------ - -* :doc:`/components/console/usage` -* :doc:`/components/console/single_command_tool` -* :doc:`/components/console/changing_default_command` -* :doc:`/components/console/events` -* :doc:`/components/console/console_arguments` - -.. _Packagist: https://packagist.org/packages/symfony/console -.. _ConEmu: https://code.google.com/p/conemu-maximus5/ -.. _ANSICON: https://github.com/adoxa/ansicon/releases diff --git a/components/console/logger.rst b/components/console/logger.rst index 8a713189b05..2c44ee75970 100644 --- a/components/console/logger.rst +++ b/components/console/logger.rst @@ -9,7 +9,7 @@ The Console component comes with a standalone logger complying with the be sent to the :class:`Symfony\\Component\\Console\\Output\\OutputInterface` instance passed as a parameter to the constructor. -The logger does not have any external dependency except ``php-fig/log``. +The logger does not have any external dependency except ``psr/log``. This is useful for console applications and commands needing a lightweight PSR-3 compliant logger:: @@ -75,9 +75,9 @@ may not be sent to the :class:`Symfony\\Component\\Console\\Output\\OutputInterf instance. By default, the console logger behaves like the -:doc:`Monolog's Console Handler `. +:doc:`Monolog's Console Handler `. The association between the log level and the verbosity can be configured -through the second parameter of the :class:`Symfony\\Component\\Console\\ConsoleLogger` +through the second parameter of the :class:`Symfony\\Component\\Console\\Logger\\ConsoleLogger` constructor:: use Psr\Log\LogLevel; @@ -98,9 +98,9 @@ constructor:: // ... $formatLevelMap = array( - LogLevel::CRITICAL => ConsoleLogger::INFO, - LogLevel::DEBUG => ConsoleLogger::ERROR, + LogLevel::CRITICAL => ConsoleLogger::ERROR, + LogLevel::DEBUG => ConsoleLogger::INFO, ); $logger = new ConsoleLogger($output, array(), $formatLevelMap); -.. _PSR-3: http://www.php-fig.org/psr/psr-3/ +.. _PSR-3: https://www.php-fig.org/psr/psr-3/ diff --git a/components/console/single_command_tool.rst b/components/console/single_command_tool.rst index 609f7a0c2e3..a41f7f479d4 100644 --- a/components/console/single_command_tool.rst +++ b/components/console/single_command_tool.rst @@ -35,7 +35,7 @@ it is possible to remove this need by extending the application:: */ protected function getDefaultCommands() { - // Keep the core default commands to have the HelpCommand + // Keeps the core default commands to have the HelpCommand // which is used when using the --help option $defaultCommands = parent::getDefaultCommands(); @@ -51,7 +51,7 @@ it is possible to remove this need by extending the application:: public function getDefinition() { $inputDefinition = parent::getDefinition(); - // clear out the normal first argument, which is the command name + // clears out the normal first argument, which is the command name $inputDefinition->setArguments(); return $inputDefinition; diff --git a/components/console/usage.rst b/components/console/usage.rst index f2dbfcd43d4..d93c53647db 100644 --- a/components/console/usage.rst +++ b/components/console/usage.rst @@ -16,6 +16,8 @@ built-in options as well as a couple of built-in commands for the Console compon ` (``symfony/css-selector`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/CssSelector). + $ composer require symfony/css-selector + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -34,7 +35,7 @@ long and unwieldy expressions. Many developers -- particularly web developers -- are more comfortable using CSS selectors to find elements. As well as working in stylesheets, -CSS selectors are used in JavaScript with the ``querySelectorAll`` function +CSS selectors are used in JavaScript with the ``querySelectorAll()`` function and in popular JavaScript libraries such as jQuery, Prototype and MooTools. CSS selectors are less powerful than XPath, but far easier to write, read @@ -92,3 +93,9 @@ Several pseudo-classes are not yet supported: name (e.g. ``li:first-of-type``) but not with ``*``. .. _Packagist: https://packagist.org/packages/symfony/css-selector + +Learn more +---------- + +* :doc:`/testing` +* :doc:`/components/dom_crawler` diff --git a/components/debug/introduction.rst b/components/debug.rst similarity index 71% rename from components/debug/introduction.rst rename to components/debug.rst index f65bd877a8b..e42e3d3423a 100644 --- a/components/debug/introduction.rst +++ b/components/debug.rst @@ -14,10 +14,11 @@ The Debug Component Installation ------------ -You can install the component in many different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/debug`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Debug). + $ composer require symfony/debug + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -33,7 +34,7 @@ Enabling them all is as easy as it can get:: The :method:`Symfony\\Component\\Debug\\Debug::enable` method registers an error handler, an exception handler and -:doc:`a special class loader `. +:ref:`a special class loader `. Read the following sections for more information about the different available tools. @@ -69,8 +70,25 @@ and more useful:: .. note:: - If the :doc:`HttpFoundation component ` is + If the :doc:`HttpFoundation component ` is available, the handler uses a Symfony Response object; if not, it falls back to a regular PHP response. +.. _component-debug-class-loader: + +Debugging a Class Loader +------------------------ + +The :class:`Symfony\\Component\\Debug\\DebugClassLoader` attempts to +throw more helpful exceptions when a class isn't found by the registered +autoloaders. All autoloaders that implement a ``findFile()`` method are replaced +with a ``DebugClassLoader`` wrapper. + +Using the ``DebugClassLoader`` is as easy as calling its static +:method:`Symfony\\Component\\Debug\\DebugClassLoader::enable` method:: + + use Symfony\Component\Debug\DebugClassLoader; + + DebugClassLoader::enable(); + .. _Packagist: https://packagist.org/packages/symfony/debug diff --git a/components/debug/class_loader.rst b/components/debug/class_loader.rst deleted file mode 100644 index ff906101ed1..00000000000 --- a/components/debug/class_loader.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. index:: - single: Class Loader; DebugClassLoader - single: Debug; DebugClassLoader - -Debugging a Class Loader -======================== - -The :class:`Symfony\\Component\\Debug\\DebugClassLoader` attempts to -throw more helpful exceptions when a class isn't found by the registered -autoloaders. All autoloaders that implement a ``findFile()`` method are replaced -with a ``DebugClassLoader`` wrapper. - -Using the ``DebugClassLoader`` is as easy as calling its static -:method:`Symfony\\Component\\Debug\\DebugClassLoader::enable` method:: - - use Symfony\Component\Debug\DebugClassLoader; - - DebugClassLoader::enable(); diff --git a/components/debug/index.rst b/components/debug/index.rst deleted file mode 100644 index 6797789cb1b..00000000000 --- a/components/debug/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Debug -===== - -.. toctree:: - :maxdepth: 2 - - introduction - class_loader diff --git a/components/dependency_injection/introduction.rst b/components/dependency_injection.rst similarity index 76% rename from components/dependency_injection/introduction.rst rename to components/dependency_injection.rst index 58fe212780d..a0e712dac83 100644 --- a/components/dependency_injection/introduction.rst +++ b/components/dependency_injection.rst @@ -1,4 +1,4 @@ -.. index:: +.. index:: single: DependencyInjection single: Components; DependencyInjection @@ -9,15 +9,16 @@ The DependencyInjection Component the way objects are constructed in your application. For an introduction to Dependency Injection and service containers see -:doc:`/book/service_container`. +:doc:`/service_container`. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/dependency-injection`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/DependencyInjection). + $ composer require symfony/dependency-injection + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -43,8 +44,8 @@ You can register this in the container as a service:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $container = new ContainerBuilder(); - $container->register('mailer', 'Mailer'); + $containerBuilder = new ContainerBuilder(); + $containerBuilder->register('mailer', 'Mailer'); An improvement to the class to make it more flexible would be to allow the container to set the ``transport`` used. If you change the class @@ -66,24 +67,24 @@ Then you can set the choice of transport in the container:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $container = new ContainerBuilder(); - $container + $containerBuilder = new ContainerBuilder(); + $containerBuilder ->register('mailer', 'Mailer') ->addArgument('sendmail'); This class is now much more flexible as you have separated the choice of transport out of the implementation and into the container. -Which mail transport you have chosen may be something other services need to -know about. You can avoid having to change it in multiple places by making -it a parameter in the container and then referring to this parameter for the -``Mailer`` service's constructor argument:: +Which mail transport you have chosen may be something other services need +to know about. You can avoid having to change it in multiple places by making +it a parameter in the container and then referring to this parameter for +the ``Mailer`` service's constructor argument:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $container = new ContainerBuilder(); - $container->setParameter('mailer.transport', 'sendmail'); - $container + $containerBuilder = new ContainerBuilder(); + $containerBuilder->setParameter('mailer.transport', 'sendmail'); + $containerBuilder ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); @@ -103,19 +104,21 @@ like this:: // ... } -Then you can register this as a service as well and pass the ``mailer`` service into it:: +When defining the ``newsletter_manager`` service, the ``mailer`` service does +not exist yet. Use the ``Reference`` class to tell the container to inject the +``mailer`` service when it initializes the newsletter manager:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - $container = new ContainerBuilder(); + $containerBuilder = new ContainerBuilder(); - $container->setParameter('mailer.transport', 'sendmail'); - $container + $containerBuilder->setParameter('mailer.transport', 'sendmail'); + $containerBuilder ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); - $container + $containerBuilder ->register('newsletter_manager', 'NewsletterManager') ->addArgument(new Reference('mailer')); @@ -140,14 +143,14 @@ If you do want to though then the container can call the setter method:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; - $container = new ContainerBuilder(); + $containerBuilder = new ContainerBuilder(); - $container->setParameter('mailer.transport', 'sendmail'); - $container + $containerBuilder->setParameter('mailer.transport', 'sendmail'); + $containerBuilder ->register('mailer', 'Mailer') ->addArgument('%mailer.transport%'); - $container + $containerBuilder ->register('newsletter_manager', 'NewsletterManager') ->addMethodCall('setMailer', array(new Reference('mailer'))); @@ -156,11 +159,11 @@ like this:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $container = new ContainerBuilder(); + $containerBuilder = new ContainerBuilder(); // ... - $newsletterManager = $container->get('newsletter_manager'); + $newsletterManager = $containerBuilder->get('newsletter_manager'); Avoiding your Code Becoming Dependent on the Container ------------------------------------------------------ @@ -182,11 +185,11 @@ Setting up the Container with Configuration Files As well as setting up the services using PHP as above you can also use configuration files. This allows you to use XML or YAML to write the definitions -for the services rather than using PHP to define the services as in the above -examples. In anything but the smallest applications it makes sense to organize -the service definitions by moving them into one or more configuration files. -To do this you also need to install -:doc:`the Config component `. +for the services rather than using PHP to define the services as in the +above examples. In anything but the smallest applications it makes sense +to organize the service definitions by moving them into one or more configuration +files. To do this you also need to install +:doc:`the Config component `. Loading an XML config file:: @@ -194,8 +197,8 @@ Loading an XML config file:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; - $container = new ContainerBuilder(); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__)); + $containerBuilder = new ContainerBuilder(); + $loader = new XmlFileLoader($containerBuilder, new FileLocator(__DIR__)); $loader->load('services.xml'); Loading a YAML config file:: @@ -204,14 +207,14 @@ Loading a YAML config file:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; - $container = new ContainerBuilder(); - $loader = new YamlFileLoader($container, new FileLocator(__DIR__)); + $containerBuilder = new ContainerBuilder(); + $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__)); $loader->load('services.yml'); .. note:: If you want to load YAML config files then you will also need to install - :doc:`the Yaml component `. + :doc:`the Yaml component `. If you *do* want to use PHP to create the services then you can move this into a separate config file and load it in a similar way:: @@ -220,8 +223,8 @@ into a separate config file and load it in a similar way:: use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; - $container = new ContainerBuilder(); - $loader = new PhpFileLoader($container, new FileLocator(__DIR__)); + $containerBuilder = new ContainerBuilder(); + $loader = new PhpFileLoader($containerBuilder, new FileLocator(__DIR__)); $loader->load('services.php'); You can now set up the ``newsletter_manager`` and ``mailer`` services using @@ -238,11 +241,11 @@ config files: services: mailer: class: Mailer - arguments: ["%mailer.transport%"] + arguments: ['%mailer.transport%'] newsletter_manager: class: NewsletterManager calls: - - [setMailer, ["@mailer"]] + - [setMailer, ['@mailer']] .. code-block:: xml @@ -283,4 +286,14 @@ config files: ->register('newsletter_manager', 'NewsletterManager') ->addMethodCall('setMailer', array(new Reference('mailer'))); +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/dependency_injection/* + /service_container/* + .. _Packagist: https://packagist.org/packages/symfony/dependency-injection diff --git a/components/dependency_injection/_imports-parameters-note.rst.inc b/components/dependency_injection/_imports-parameters-note.rst.inc index 2f35ec5bdef..dc9917d370e 100644 --- a/components/dependency_injection/_imports-parameters-note.rst.inc +++ b/components/dependency_injection/_imports-parameters-note.rst.inc @@ -1,8 +1,8 @@ .. note:: - Due to the way in which parameters are resolved, you cannot use them to - build paths in imports dynamically. This means that something like the - following doesn't work: + Due to the way in which parameters are resolved, you cannot use them + to build paths in imports dynamically. This means that something like + the following doesn't work: .. configuration-block:: @@ -10,7 +10,7 @@ # app/config/config.yml imports: - - { resource: "%kernel.root_dir%/parameters.yml" } + - { resource: '%kernel.root_dir%/parameters.yml' } .. code-block:: xml diff --git a/components/dependency_injection/advanced.rst b/components/dependency_injection/advanced.rst deleted file mode 100644 index 8510280432e..00000000000 --- a/components/dependency_injection/advanced.rst +++ /dev/null @@ -1,220 +0,0 @@ -.. index:: - single: DependencyInjection; Advanced configuration - -Advanced Container Configuration -================================ - -.. _container-private-services: - -Marking Services as public / private ------------------------------------- - -When defining services, you'll usually want to be able to access these definitions -within your application code. These services are called ``public``. For example, -the ``doctrine`` service registered with the container when using the DoctrineBundle -is a public service. This means that you can fetch it from the container -using the ``get()`` method:: - - $doctrine = $container->get('doctrine'); - -In some cases, a service *only* exists to be injected into another service -and is *not* intended to be fetched directly from the container as shown -above. - -.. _inlined-private-services: - -In these cases, to get a minor performance boost, you can set the service -to be *not* public (i.e. private): - -.. configuration-block:: - - .. code-block:: yaml - - services: - foo: - class: Example\Foo - public: false - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $definition = new Definition('Example\Foo'); - $definition->setPublic(false); - $container->setDefinition('foo', $definition); - -What makes private services special is that, if they are only injected once, -they are converted from services to inlined instantiations (e.g. ``new PrivateThing()``). -This increases the container's performance. - -Now that the service is private, you *should not* fetch the service directly -from the container:: - - $container->get('foo'); - -This *may or may not work*, depending on if the service could be inlined. -Simply said: A service can be marked as private if you do not want to access -it directly from your code. - -However, if a service has been marked as private, you can still alias it (see -below) to access this service (via the alias). - -.. note:: - - Services are by default public. - -Aliasing --------- - -You may sometimes want to use shortcuts to access some services. You can -do so by aliasing them and, furthermore, you can even alias non-public -services. - -.. configuration-block:: - - .. code-block:: yaml - - services: - foo: - class: Example\Foo - bar: - alias: foo - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $container->setDefinition('foo', new Definition('Example\Foo')); - - $containerBuilder->setAlias('bar', 'foo'); - -This means that when using the container directly, you can access the ``foo`` -service by asking for the ``bar`` service like this:: - - $container->get('bar'); // Would return the foo service - -.. tip:: - - In YAML, you can also use a shortcut to alias a service: - - .. code-block:: yaml - - services: - foo: - class: Example\Foo - bar: "@foo" - -Decorating Services -------------------- - -When overriding an existing definition, the old service is lost: - -.. code-block:: php - - $container->register('foo', 'FooService'); - - // this is going to replace the old definition with the new one - // old definition is lost - $container->register('foo', 'CustomFooService'); - -Most of the time, that's exactly what you want to do. But sometimes, -you might want to decorate the old one instead. In this case, the -old service should be kept around to be able to reference it in the -new one. This configuration replaces ``foo`` with a new one, but keeps -a reference of the old one as ``bar.inner``: - -.. configuration-block:: - - .. code-block:: yaml - - bar: - public: false - class: stdClass - decorates: foo - arguments: ["@bar.inner"] - - .. code-block:: xml - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Reference; - - $container->register('bar', 'stdClass') - ->addArgument(new Reference('bar.inner')) - ->setPublic(false) - ->setDecoratedService('foo'); - -Here is what's going on here: the ``setDecoratedService()`` method tells -the container that the ``bar`` service should replace the ``foo`` service, -renaming ``foo`` to ``bar.inner``. -By convention, the old ``foo`` service is going to be renamed ``bar.inner``, -so you can inject it into your new service. - -.. note:: - The generated inner id is based on the id of the decorator service - (``bar`` here), not of the decorated service (``foo`` here). This is - mandatory to allow several decorators on the same service (they need to have - different generated inner ids). - - Most of the time, the decorator should be declared private, as you will not - need to retrieve it as ``bar`` from the container. The visibility of the - decorated ``foo`` service (which is an alias for ``bar``) will still be the - same as the original ``foo`` visibility. - -You can change the inner service name if you want to: - -.. configuration-block:: - - .. code-block:: yaml - - bar: - class: stdClass - public: false - decorates: foo - decoration_inner_name: bar.wooz - arguments: ["@bar.wooz"] - - .. code-block:: xml - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Reference; - - $container->register('bar', 'stdClass') - ->addArgument(new Reference('bar.wooz')) - ->setPublic(false) - ->setDecoratedService('foo', 'bar.wooz'); diff --git a/components/dependency_injection/compilation.rst b/components/dependency_injection/compilation.rst index 67ba5c3e20a..32dc771931b 100644 --- a/components/dependency_injection/compilation.rst +++ b/components/dependency_injection/compilation.rst @@ -1,5 +1,5 @@ -.. index:: - single: DependencyInjection; Compilation +.. index:: + single: DependencyInjection; Compilation Compiling the Container ======================= @@ -8,8 +8,8 @@ The service container can be compiled for various reasons. These reasons include checking for any potential issues such as circular references and making the container more efficient by resolving parameters and removing unused services. Also, certain features - like using -:doc:`parent services ` - -require the container to be compiled. +:doc:`parent services ` +- require the container to be compiled. It is compiled by running:: @@ -17,12 +17,13 @@ It is compiled by running:: The compile method uses *Compiler Passes* for the compilation. The DependencyInjection component comes with several passes which are automatically registered for -compilation. For example the :class:`Symfony\\Component\\DependencyInjection\\Compiler\\CheckDefinitionValidityPass` -checks for various potential issues with the definitions that have been set -in the container. After this and several other passes that check the container's -validity, further compiler passes are used to optimize the configuration -before it is cached. For example, private services and abstract services -are removed, and aliases are resolved. +compilation. For example the +:class:`Symfony\\Component\\DependencyInjection\\Compiler\\CheckDefinitionValidityPass` +checks for various potential issues with the definitions that have been +set in the container. After this and several other passes that check the +container's validity, further compiler passes are used to optimize the +configuration before it is cached. For example, private services and abstract +services are removed and aliases are resolved. .. _components-dependency-injection-extension: @@ -30,27 +31,29 @@ Managing Configuration with Extensions -------------------------------------- As well as loading configuration directly into the container as shown in -:doc:`/components/dependency_injection/introduction`, you can manage it by -registering extensions with the container. The first step in the compilation +:doc:`/components/dependency_injection`, you can manage it +by registering extensions with the container. The first step in the compilation process is to load configuration from any extension classes registered with the container. Unlike the configuration loaded directly, they are only processed when the container is compiled. If your application is modular then extensions allow each module to register and manage their own service configuration. -The extensions must implement :class:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface` +The extensions must implement +:class:`Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface` and can be registered with the container with:: $container->registerExtension($extension); -The main work of the extension is done in the ``load`` method. In the ``load`` method -you can load configuration from one or more configuration files as well as -manipulate the container definitions using the methods shown in :doc:`/components/dependency_injection/definitions`. +The main work of the extension is done in the ``load()`` method. In the ``load()`` +method you can load configuration from one or more configuration files as +well as manipulate the container definitions using the methods shown in +:doc:`/service_container/definitions`. -The ``load`` method is passed a fresh container to set up, which is then -merged afterwards into the container it is registered with. This allows you -to have several extensions managing container definitions independently. -The extensions do not add to the containers configuration when they are added -but are processed when the container's ``compile`` method is called. +The ``load()`` method is passed a fresh container to set up, which is then +merged afterwards into the container it is registered with. This allows +you to have several extensions managing container definitions independently. +The extensions do not add to the containers configuration when they are +added but are processed when the container's ``compile()`` method is called. A very simple extension may just load configuration files into the container:: @@ -73,16 +76,16 @@ A very simple extension may just load configuration files into the container:: // ... } -This does not gain very much compared to loading the file directly into the -overall container being built. It just allows the files to be split up amongst -the modules/bundles. Being able to affect the configuration of a module from -configuration files outside of the module/bundle is needed to make a complex -application configurable. This can be done by specifying sections of config files -loaded directly into the container as being for a particular extension. These -sections on the config will not be processed directly by the container but by the -relevant Extension. +This does not gain very much compared to loading the file directly into +the overall container being built. It just allows the files to be split +up amongst the modules/bundles. Being able to affect the configuration +of a module from configuration files outside of the module/bundle is needed +to make a complex application configurable. This can be done by specifying +sections of config files loaded directly into the container as being for +a particular extension. These sections on the config will not be processed +directly by the container but by the relevant Extension. -The Extension must specify a ``getAlias`` method to implement the interface:: +The Extension must specify a ``getAlias()`` method to implement the interface:: // ... @@ -96,8 +99,8 @@ The Extension must specify a ``getAlias`` method to implement the interface:: } } -For YAML configuration files specifying the alias for the Extension as a key -will mean that those values are passed to the Extension's ``load`` method: +For YAML configuration files specifying the alias for the extension as a +key will mean that those values are passed to the Extension's ``load()`` method: .. code-block:: yaml @@ -106,21 +109,22 @@ will mean that those values are passed to the Extension's ``load`` method: foo: fooValue bar: barValue -If this file is loaded into the configuration then the values in it are only -processed when the container is compiled at which point the Extensions are loaded:: +If this file is loaded into the configuration then the values in it are +only processed when the container is compiled at which point the Extensions +are loaded:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; - $container = new ContainerBuilder(); - $container->registerExtension(new AcmeDemoExtension); + $containerBuilder = new ContainerBuilder(); + $containerBuilder->registerExtension(new AcmeDemoExtension); - $loader = new YamlFileLoader($container, new FileLocator(__DIR__)); + $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__)); $loader->load('config.yml'); // ... - $container->compile(); + $containerBuilder->compile(); .. note:: @@ -129,7 +133,7 @@ processed when the container is compiled at which point the Extensions are loade or an exception will be thrown. The values from those sections of the config files are passed into the first -argument of the ``load`` method of the extension:: +argument of the ``load()`` method of the extension:: public function load(array $configs, ContainerBuilder $container) { @@ -138,9 +142,9 @@ argument of the ``load`` method of the extension:: } The ``$configs`` argument is an array containing each different config file -that was loaded into the container. You are only loading a single config file -in the above example but it will still be within an array. The array will look -like this:: +that was loaded into the container. You are only loading a single config +file in the above example but it will still be within an array. The array +will look like this:: array( array( @@ -150,9 +154,9 @@ like this:: ) Whilst you can manually manage merging the different files, it is much better -to use :doc:`the Config component ` to merge -and validate the config values. Using the configuration processing you could -access the config value this way:: +to use :doc:`the Config component ` to +merge and validate the config values. Using the configuration processing +you could access the config value this way:: use Symfony\Component\Config\Definition\Processor; // ... @@ -186,7 +190,7 @@ the XML configuration:: .. note:: - XSD validation is optional, returning ``false`` from the ``getXsdValidationBasePath`` + XSD validation is optional, returning ``false`` from the ``getXsdValidationBasePath()`` method will disable it. The XML version of the config would then look like this: @@ -200,20 +204,20 @@ The XML version of the config would then look like this: xsi:schemaLocation="http://www.example.com/symfony/schema/ http://www.example.com/symfony/schema/hello-1.0.xsd"> - fooValue + fooValue barValue .. note:: - In the Symfony full-stack Framework there is a base Extension class which - implements these methods as well as a shortcut method for processing the - configuration. See :doc:`/cookbook/bundles/extension` for more details. + In the Symfony full-stack Framework there is a base Extension class + which implements these methods as well as a shortcut method for processing + the configuration. See :doc:`/bundles/extension` for more details. -The processed config value can now be added as container parameters as if it were -listed in a ``parameters`` section of the config file but with the additional -benefit of merging multiple files and validation of the configuration:: +The processed config value can now be added as container parameters as if +it were listed in a ``parameters`` section of the config file but with the +additional benefit of merging multiple files and validation of the configuration:: public function load(array $configs, ContainerBuilder $container) { @@ -227,8 +231,8 @@ benefit of merging multiple files and validation of the configuration:: } More complex configuration requirements can be catered for in the Extension -classes. For example, you may choose to load a main service configuration file -but also load a secondary one only if a certain parameter is set:: +classes. For example, you may choose to load a main service configuration +file but also load a secondary one only if a certain parameter is set:: public function load(array $configs, ContainerBuilder $container) { @@ -259,11 +263,11 @@ but also load a secondary one only if a certain parameter is set:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $container = new ContainerBuilder(); + $containerBuilder = new ContainerBuilder(); $extension = new AcmeDemoExtension(); - $container->registerExtension($extension); - $container->loadFromExtension($extension->getAlias()); - $container->compile(); + $containerBuilder->registerExtension($extension); + $containerBuilder->loadFromExtension($extension->getAlias()); + $containerBuilder->compile(); .. note:: @@ -278,7 +282,8 @@ Prepending Configuration Passed to the Extension ------------------------------------------------ An Extension can prepend the configuration of any Bundle before the ``load()`` -method is called by implementing :class:`Symfony\\Component\\DependencyInjection\\Extension\\PrependExtensionInterface`:: +method is called by implementing +:class:`Symfony\\Component\\DependencyInjection\\Extension\\PrependExtensionInterface`:: use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; // ... @@ -287,7 +292,7 @@ method is called by implementing :class:`Symfony\\Component\\DependencyInjection { // ... - public function prepend() + public function prepend(ContainerBuilder $container) { // ... @@ -297,8 +302,9 @@ method is called by implementing :class:`Symfony\\Component\\DependencyInjection } } -For more details, see :doc:`/cookbook/bundles/prepend_extension`, which is -specific to the Symfony Framework, but contains more details about this feature. +For more details, see :doc:`/bundles/prepend_extension`, which +is specific to the Symfony Framework, but contains more details about this +feature. Creating a Compiler Pass ------------------------ @@ -306,11 +312,11 @@ Creating a Compiler Pass You can also create and register your own compiler passes with the container. To create a compiler pass it needs to implement the :class:`Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface` -interface. The compiler pass gives you an opportunity to manipulate the service -definitions that have been compiled. This can be very powerful, but is not -something needed in everyday use. +interface. The compiler pass gives you an opportunity to manipulate the +service definitions that have been compiled. This can be very powerful, +but is not something needed in everyday use. -The compiler pass must have the ``process`` method which is passed the container +The compiler pass must have the ``process()`` method which is passed the container being compiled:: use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -325,10 +331,10 @@ being compiled:: } The container's parameters and definitions can be manipulated using the -methods described in the :doc:`/components/dependency_injection/definitions`. -One common thing to do in a compiler pass is to search for all services that -have a certain tag in order to process them in some way or dynamically plug -each into some other service. +methods described in the :doc:`/service_container/definitions`. One common +thing to do in a compiler pass is to search for all services that have a +certain tag in order to process them in some way or dynamically plug each into +some other service. Registering a Compiler Pass --------------------------- @@ -338,13 +344,13 @@ will then be called when the container is compiled:: use Symfony\Component\DependencyInjection\ContainerBuilder; - $container = new ContainerBuilder(); - $container->addCompilerPass(new CustomCompilerPass); + $containerBuilder = new ContainerBuilder(); + $containerBuilder->addCompilerPass(new CustomCompilerPass); .. note:: Compiler passes are registered differently if you are using the full-stack - framework, see :doc:`/cookbook/service_container/compiler_passes` for + framework, see :doc:`/service_container/compiler_passes` for more details. Controlling the Pass Ordering @@ -352,9 +358,10 @@ Controlling the Pass Ordering The default compiler passes are grouped into optimization passes and removal passes. The optimization passes run first and include tasks such as resolving -references within the definitions. The removal passes perform tasks such as removing -private aliases and unused services. You can choose where in the order any custom -passes you add are run. By default they will be run before the optimization passes. +references within the definitions. The removal passes perform tasks such +as removing private aliases and unused services. You can choose where in +the order any custom passes you add are run. By default they will be run +before the optimization passes. You can use the following constants as the second argument when registering a pass with the container to control where it goes in the order: @@ -365,13 +372,14 @@ a pass with the container to control where it goes in the order: * ``PassConfig::TYPE_REMOVE`` * ``PassConfig::TYPE_AFTER_REMOVING`` -For example, to run your custom pass after the default removal passes have been run:: +For example, to run your custom pass after the default removal passes have +been run:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\PassConfig; - $container = new ContainerBuilder(); - $container->addCompilerPass( + $containerBuilder = new ContainerBuilder(); + $containerBuilder->addCompilerPass( new CustomCompilerPass, PassConfig::TYPE_AFTER_REMOVING ); @@ -382,12 +390,13 @@ Dumping the Configuration for Performance ----------------------------------------- Using configuration files to manage the service container can be much easier -to understand than using PHP once there are a lot of services. This ease comes -at a price though when it comes to performance as the config files need to be -parsed and the PHP configuration built from them. The compilation process makes -the container more efficient but it takes time to run. You can have the best of both -worlds though by using configuration files and then dumping and caching the resulting -configuration. The ``PhpDumper`` makes dumping the compiled container easy:: +to understand than using PHP once there are a lot of services. This ease +comes at a price though when it comes to performance as the config files +need to be parsed and the PHP configuration built from them. The compilation +process makes the container more efficient but it takes time to run. You +can have the best of both worlds though by using configuration files and +then dumping and caching the resulting configuration. The ``PhpDumper`` +makes dumping the compiled container easy:: use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; @@ -398,17 +407,17 @@ configuration. The ``PhpDumper`` makes dumping the compiled container easy:: require_once $file; $container = new ProjectServiceContainer(); } else { - $container = new ContainerBuilder(); + $containerBuilder = new ContainerBuilder(); // ... - $container->compile(); + $containerBuilder->compile(); - $dumper = new PhpDumper($container); + $dumper = new PhpDumper($containerBuilder); file_put_contents($file, $dumper->dump()); } ``ProjectServiceContainer`` is the default name given to the dumped container -class, you can change this though this with the ``class`` option when you dump -it:: +class. However you can change this with the ``class`` option when you +dump it:: // ... $file = __DIR__ .'/cache/container.php'; @@ -417,25 +426,26 @@ it:: require_once $file; $container = new MyCachedContainer(); } else { - $container = new ContainerBuilder(); + $containerBuilder = new ContainerBuilder(); // ... - $container->compile(); + $containerBuilder->compile(); - $dumper = new PhpDumper($container); + $dumper = new PhpDumper($containerBuilder); file_put_contents( $file, $dumper->dump(array('class' => 'MyCachedContainer')) ); } -You will now get the speed of the PHP configured container with the ease of using -configuration files. Additionally dumping the container in this way further optimizes -how the services are created by the container. +You will now get the speed of the PHP configured container with the ease +of using configuration files. Additionally dumping the container in this +way further optimizes how the services are created by the container. In the above example you will need to delete the cached container file whenever -you make any changes. Adding a check for a variable that determines if you are -in debug mode allows you to keep the speed of the cached container in production -but getting an up to date configuration whilst developing your application:: +you make any changes. Adding a check for a variable that determines if you +are in debug mode allows you to keep the speed of the cached container in +production but getting an up to date configuration whilst developing your +application:: // ... @@ -448,12 +458,12 @@ but getting an up to date configuration whilst developing your application:: require_once $file; $container = new MyCachedContainer(); } else { - $container = new ContainerBuilder(); + $containerBuilder = new ContainerBuilder(); // ... - $container->compile(); + $containerBuilder->compile(); if (!$isDebug) { - $dumper = new PhpDumper($container); + $dumper = new PhpDumper($containerBuilder); file_put_contents( $file, $dumper->dump(array('class' => 'MyCachedContainer')) @@ -468,11 +478,11 @@ the container in the way described in ":doc:`/components/config/caching`" in the config component documentation. You do not need to work out which files to cache as the container builder -keeps track of all the resources used to configure it, not just the configuration -files but the extension classes and compiler passes as well. This means that -any changes to any of these files will invalidate the cache and trigger the -container being rebuilt. You just need to ask the container for these resources -and use them as metadata for the cache:: +keeps track of all the resources used to configure it, not just the +configuration files but the extension classes and compiler passes as well. +This means that any changes to any of these files will invalidate the cache +and trigger the container being rebuilt. You just need to ask the container +for these resources and use them as metadata for the cache:: // ... @@ -497,12 +507,13 @@ and use them as metadata for the cache:: require_once $file; $container = new MyCachedContainer(); -Now the cached dumped container is used regardless of whether debug mode is on or not. -The difference is that the ``ConfigCache`` is set to debug mode with its second -constructor argument. When the cache is not in debug mode the cached container -will always be used if it exists. In debug mode, an additional metadata file -is written with the timestamps of all the resource files. These are then checked -to see if the files have changed, if they have the cache will be considered stale. +Now the cached dumped container is used regardless of whether debug mode +is on or not. The difference is that the ``ConfigCache`` is set to debug +mode with its second constructor argument. When the cache is not in debug +mode the cached container will always be used if it exists. In debug mode, +an additional metadata file is written with the timestamps of all the resource +files. These are then checked to see if the files have changed, if they +have the cache will be considered stale. .. note:: diff --git a/components/dependency_injection/configurators.rst b/components/dependency_injection/configurators.rst deleted file mode 100644 index c8036fc8685..00000000000 --- a/components/dependency_injection/configurators.rst +++ /dev/null @@ -1,221 +0,0 @@ -.. index:: - single: DependencyInjection; Service configurators - -Configuring Services with a Service Configurator -================================================ - -The Service Configurator is a feature of the Dependency Injection Container that -allows you to use a callable to configure a service after its instantiation. - -You can specify a method in another service, a PHP function or a static method -in a class. The service instance is passed to the callable, allowing the -configurator to do whatever it needs to configure the service after its -creation. - -A Service Configurator can be used, for example, when you have a service that -requires complex setup based on configuration settings coming from different -sources/services. Using an external configurator, you can maintain the service -implementation cleanly and keep it decoupled from the other objects that provide -the configuration needed. - -Another interesting use case is when you have multiple objects that share a -common configuration or that should be configured in a similar way at runtime. - -For example, suppose you have an application where you send different types of -emails to users. Emails are passed through different formatters that could be -enabled or not depending on some dynamic application settings. You start -defining a ``NewsletterManager`` class like this:: - - class NewsletterManager implements EmailFormatterAwareInterface - { - protected $mailer; - protected $enabledFormatters; - - public function setMailer(Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function setEnabledFormatters(array $enabledFormatters) - { - $this->enabledFormatters = $enabledFormatters; - } - - // ... - } - -and also a ``GreetingCardManager`` class:: - - class GreetingCardManager implements EmailFormatterAwareInterface - { - protected $mailer; - protected $enabledFormatters; - - public function setMailer(Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function setEnabledFormatters(array $enabledFormatters) - { - $this->enabledFormatters = $enabledFormatters; - } - - // ... - } - -As mentioned before, the goal is to set the formatters at runtime depending on -application settings. To do this, you also have an ``EmailFormatterManager`` -class which is responsible for loading and validating formatters enabled -in the application:: - - class EmailFormatterManager - { - protected $enabledFormatters; - - public function loadFormatters() - { - // code to configure which formatters to use - $enabledFormatters = array(...); - // ... - - $this->enabledFormatters = $enabledFormatters; - } - - public function getEnabledFormatters() - { - return $this->enabledFormatters; - } - - // ... - } - -If your goal is to avoid having to couple ``NewsletterManager`` and -``GreetingCardManager`` with ``EmailFormatterManager``, then you might want to -create a configurator class to configure these instances:: - - class EmailConfigurator - { - private $formatterManager; - - public function __construct(EmailFormatterManager $formatterManager) - { - $this->formatterManager = $formatterManager; - } - - public function configure(EmailFormatterAwareInterface $emailManager) - { - $emailManager->setEnabledFormatters( - $this->formatterManager->getEnabledFormatters() - ); - } - - // ... - } - -The ``EmailConfigurator``'s job is to inject the enabled filters into ``NewsletterManager`` -and ``GreetingCardManager`` because they are not aware of where the enabled -filters come from. In the other hand, the ``EmailFormatterManager`` holds the -knowledge about the enabled formatters and how to load them, keeping the single -responsibility principle. - -Configurator Service Config ---------------------------- - -The service config for the above classes would look something like this: - -.. configuration-block:: - - .. code-block:: yaml - - services: - my_mailer: - # ... - - email_formatter_manager: - class: EmailFormatterManager - # ... - - email_configurator: - class: EmailConfigurator - arguments: ["@email_formatter_manager"] - # ... - - newsletter_manager: - class: NewsletterManager - calls: - - [setMailer, ["@my_mailer"]] - configurator: ["@email_configurator", configure] - - greeting_card_manager: - class: GreetingCardManager - calls: - - [setMailer, ["@my_mailer"]] - configurator: ["@email_configurator", configure] - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container->setDefinition('my_mailer', ...); - $container->setDefinition('email_formatter_manager', new Definition( - 'EmailFormatterManager' - )); - $container->setDefinition('email_configurator', new Definition( - 'EmailConfigurator' - )); - $container->setDefinition('newsletter_manager', new Definition( - 'NewsletterManager' - ))->addMethodCall('setMailer', array( - new Reference('my_mailer'), - ))->setConfigurator(array( - new Reference('email_configurator'), - 'configure', - ))); - $container->setDefinition('greeting_card_manager', new Definition( - 'GreetingCardManager' - ))->addMethodCall('setMailer', array( - new Reference('my_mailer'), - ))->setConfigurator(array( - new Reference('email_configurator'), - 'configure', - ))); diff --git a/components/dependency_injection/definitions.rst b/components/dependency_injection/definitions.rst deleted file mode 100644 index 9c9574c7b9b..00000000000 --- a/components/dependency_injection/definitions.rst +++ /dev/null @@ -1,139 +0,0 @@ -.. index:: - single: DependencyInjection; Service definitions - -Working with Container Service Definitions -========================================== - -Getting and Setting Service Definitions ---------------------------------------- - -There are some helpful methods for working with the service definitions. - -To find out if there is a definition for a service id:: - - $container->hasDefinition($serviceId); - -This is useful if you only want to do something if a particular definition exists. - -You can retrieve a definition with:: - - $container->getDefinition($serviceId); - -or:: - - $container->findDefinition($serviceId); - -which unlike ``getDefinition()`` also resolves aliases so if the ``$serviceId`` -argument is an alias you will get the underlying definition. - -The service definitions themselves are objects so if you retrieve a definition -with these methods and make changes to it these will be reflected in the -container. If, however, you are creating a new definition then you can add -it to the container using:: - - $container->setDefinition($id, $definition); - -Working with a Definition -------------------------- - -Creating a new Definition -~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to create a new definition rather than manipulate one retrieved -from the container then the definition class is :class:`Symfony\\Component\\DependencyInjection\\Definition`. - -Class -~~~~~ - -First up is the class of a definition, this is the class of the object returned -when the service is requested from the container. - -To find out what class is set for a definition:: - - $definition->getClass(); - -and to set a different class:: - - $definition->setClass($class); // Fully qualified class name as string - -Constructor Arguments -~~~~~~~~~~~~~~~~~~~~~ - -To get an array of the constructor arguments for a definition you can use:: - - $definition->getArguments(); - -or to get a single argument by its position:: - - $definition->getArgument($index); - // e.g. $definition->getArgument(0) for the first argument - -You can add a new argument to the end of the arguments array using:: - - $definition->addArgument($argument); - -The argument can be a string, an array, a service parameter by using ``%parameter_name%`` -or a service id by using:: - - use Symfony\Component\DependencyInjection\Reference; - - // ... - - $definition->addArgument(new Reference('service_id')); - -In a similar way you can replace an already set argument by index using:: - - $definition->replaceArgument($index, $argument); - -You can also replace all the arguments (or set some if there are none) with -an array of arguments:: - - $definition->setArguments($arguments); - -Method Calls -~~~~~~~~~~~~ - -If the service you are working with uses setter injection then you can manipulate -any method calls in the definitions as well. - -You can get an array of all the method calls with:: - - $definition->getMethodCalls(); - -Add a method call with:: - - $definition->addMethodCall($method, $arguments); - -Where ``$method`` is the method name and ``$arguments`` is an array of the arguments -to call the method with. The arguments can be strings, arrays, parameters or -service ids as with the constructor arguments. - -You can also replace any existing method calls with an array of new ones with:: - - $definition->setMethodCalls($methodCalls); - -.. tip:: - - There are more examples of specific ways of working with definitions - in the PHP code blocks of the configuration examples on pages such as - :doc:`/components/dependency_injection/factories` and - :doc:`/components/dependency_injection/parentservices`. - -.. note:: - - The methods here that change service definitions can only be used before - the container is compiled. Once the container is compiled you cannot - manipulate service definitions further. To learn more about compiling - the container see :doc:`/components/dependency_injection/compilation`. - -Requiring Files -~~~~~~~~~~~~~~~ - -There might be use cases when you need to include another file just before -the service itself gets loaded. To do so, you can use the -:method:`Symfony\\Component\\DependencyInjection\\Definition::setFile` method:: - - $definition->setFile('/src/path/to/file/foo.php'); - -Notice that Symfony will internally call the PHP statement ``require_once``, -which means that your file will be included only once per request. diff --git a/components/dependency_injection/factories.rst b/components/dependency_injection/factories.rst deleted file mode 100644 index c778093bbe9..00000000000 --- a/components/dependency_injection/factories.rst +++ /dev/null @@ -1,181 +0,0 @@ -.. index:: - single: DependencyInjection; Factories - -Using a Factory to Create Services -================================== - -Symfony's Service Container provides a powerful way of controlling the -creation of objects, allowing you to specify arguments passed to the constructor -as well as calling methods and setting parameters. Sometimes, however, this -will not provide you with everything you need to construct your objects. -For this situation, you can use a factory to create the object and tell the -service container to call a method on the factory rather than directly instantiating -the class. - -.. versionadded:: 2.6 - The new :method:`Symfony\\Component\\DependencyInjection\\Definition::setFactory` - method was introduced in Symfony 2.6. Refer to older versions for the - syntax for factories prior to 2.6. - -Suppose you have a factory that configures and returns a new ``NewsletterManager`` -object:: - - class NewsletterManagerFactory - { - public static function createNewsletterManager() - { - $newsletterManager = new NewsletterManager(); - - // ... - - return $newsletterManager; - } - } - -To make the ``NewsletterManager`` object available as a service, you can -configure the service container to use the -``NewsletterFactory::createNewsletterManager()`` factory method: - -.. configuration-block:: - - .. code-block:: yaml - - services: - newsletter_manager: - class: NewsletterManager - factory: [NewsletterManagerFactory, createNewsletterManager] - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - // ... - $definition = new Definition('NewsletterManager'); - $definition->setFactory(array('NewsletterManagerFactory', 'createNewsletterManager')); - - $container->setDefinition('newsletter_manager', $definition); - -.. note:: - - When using a factory to create services, the value chosen for the ``class`` - option has no effect on the resulting service. The actual class name only - depends on the object that is returned by the factory. However, the configured - class name may be used by compiler passes and therefore should be set to a - sensible value. - -Now, the method will be called statically. If the factory class itself should -be instantiated and the resulting object's method called, configure the factory -itself as a service. In this case, the method (e.g. get) should be changed to -be non-static. - -.. configuration-block:: - - .. code-block:: yaml - - services: - newsletter_manager.factory: - class: NewsletterManagerFactory - newsletter_manager: - class: NewsletterManager - factory: ["@newsletter_manager.factory", createNewsletterManager] - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\DependencyInjection\Definition; - - // ... - $container->register('newsletter_manager.factory', 'NewsletterManagerFactory'); - - $newsletterManager = new Definition(); - $newsletterManager->setFactory(array( - new Reference('newsletter_manager.factory'), - 'createNewsletterManager' - )); - $container->setDefinition('newsletter_manager', $newsletterManager); - -Passing Arguments to the Factory Method ---------------------------------------- - -If you need to pass arguments to the factory method, you can use the ``arguments`` -options inside the service container. For example, suppose the ``createNewsletterManager`` -method in the previous example takes the ``templating`` service as an argument: - -.. configuration-block:: - - .. code-block:: yaml - - services: - newsletter_manager.factory: - class: NewsletterManagerFactory - - newsletter_manager: - class: NewsletterManager - factory: ["@newsletter_manager.factory", createNewsletterManager] - arguments: - - "@templating" - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\DependencyInjection\Definition; - - // ... - $container->register('newsletter_manager.factory', 'NewsletterManagerFactory'); - - $newsletterManager = new Definition( - 'NewsletterManager', - array(new Reference('templating')) - ); - $newsletterManager->setFactory(array( - new Reference('newsletter_manager.factory'), - 'createNewsletterManager' - )); - $container->setDefinition('newsletter_manager', $newsletterManager); diff --git a/components/dependency_injection/index.rst b/components/dependency_injection/index.rst deleted file mode 100644 index dfa2e1ef54b..00000000000 --- a/components/dependency_injection/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -DependencyInjection -=================== - -.. toctree:: - :maxdepth: 2 - - introduction - types - parameters - definitions - synthetic_services - compilation - tags - factories - configurators - parentservices - advanced - lazy_services - workflow diff --git a/components/dependency_injection/lazy_services.rst b/components/dependency_injection/lazy_services.rst deleted file mode 100644 index d527d02db98..00000000000 --- a/components/dependency_injection/lazy_services.rst +++ /dev/null @@ -1,118 +0,0 @@ -.. index:: - single: Dependency Injection; Lazy Services - -Lazy Services -============= - -.. versionadded:: 2.3 - Lazy services were introduced in Symfony 2.3. - -Why lazy Services? ------------------- - -In some cases, you may want to inject a service that is a bit heavy to instantiate, -but is not always used inside your object. For example, imagine you have -a ``NewsletterManager`` and you inject a ``mailer`` service into it. Only -a few methods on your ``NewsletterManager`` actually use the ``mailer``, -but even when you don't need it, a ``mailer`` service is always instantiated -in order to construct your ``NewsletterManager``. - -Configuring lazy services is one answer to this. With a lazy service, a "proxy" -of the ``mailer`` service is actually injected. It looks and acts just like -the ``mailer``, except that the ``mailer`` isn't actually instantiated until -you interact with the proxy in some way. - -Installation ------------- - -In order to use the lazy service instantiation, you will first need to install -the `ProxyManager bridge`_: - -.. code-block:: bash - - $ composer require symfony/proxy-manager-bridge:~2.3 - -.. note:: - - If you're using the full-stack framework, the proxy manager bridge is already - included but the actual proxy manager needs to be included. So, run: - - .. code-block:: bash - - $ php composer.phar require ocramius/proxy-manager:~1.0 - - Afterwards compile your container and check to make sure that you get - a proxy for your lazy services. - -Configuration -------------- - -You can mark the service as ``lazy`` by manipulating its definition: - -.. configuration-block:: - - .. code-block:: yaml - - services: - foo: - class: Acme\Foo - lazy: true - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $definition = new Definition('Acme\Foo'); - $definition->setLazy(true); - $container->setDefinition('foo', $definition); - -You can then require the service from the container:: - - $service = $container->get('foo'); - -At this point the retrieved ``$service`` should be a virtual `proxy`_ with -the same signature of the class representing the service. You can also inject -the service just like normal into other services. The object that's actually -injected will be the proxy. - -To check if your proxy works you can simply check the interface of the -received object. - -.. code-block:: php - - var_dump(class_implements($service)); - -If the class implements the ``ProxyManager\Proxy\LazyLoadingInterface`` your -lazy loaded services are working. - -.. note:: - - If you don't install the `ProxyManager bridge`_, the container will just - skip over the ``lazy`` flag and simply instantiate the service as it would - normally do. - -The proxy gets initialized and the actual service is instantiated as soon -as you interact in any way with this object. - -Additional Resources --------------------- - -You can read more about how proxies are instantiated, generated and initialized -in the `documentation of ProxyManager`_. - - -.. _`ProxyManager bridge`: https://github.com/symfony/symfony/tree/master/src/Symfony/Bridge/ProxyManager -.. _`proxy`: http://en.wikipedia.org/wiki/Proxy_pattern -.. _`documentation of ProxyManager`: https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md diff --git a/components/dependency_injection/parentservices.rst b/components/dependency_injection/parentservices.rst deleted file mode 100644 index b7729ab1f50..00000000000 --- a/components/dependency_injection/parentservices.rst +++ /dev/null @@ -1,420 +0,0 @@ -.. index:: - single: DependencyInjection; Parent services - -Managing common Dependencies with parent Services -================================================= - -As you add more functionality to your application, you may well start to have -related classes that share some of the same dependencies. For example you -may have a Newsletter Manager which uses setter injection to set its dependencies:: - - class NewsletterManager - { - protected $mailer; - protected $emailFormatter; - - public function setMailer(Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function setEmailFormatter(EmailFormatter $emailFormatter) - { - $this->emailFormatter = $emailFormatter; - } - - // ... - } - -and also a Greeting Card class which shares the same dependencies:: - - class GreetingCardManager - { - protected $mailer; - protected $emailFormatter; - - public function setMailer(Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function setEmailFormatter(EmailFormatter $emailFormatter) - { - $this->emailFormatter = $emailFormatter; - } - - // ... - } - -The service config for these classes would look something like this: - -.. configuration-block:: - - .. code-block:: yaml - - services: - my_mailer: - # ... - - my_email_formatter: - # ... - - newsletter_manager: - class: NewsletterManager - calls: - - [setMailer, ["@my_mailer"]] - - [setEmailFormatter, ["@my_email_formatter"]] - - greeting_card_manager: - class: "GreetingCardManager" - calls: - - [setMailer, ["@my_mailer"]] - - [setEmailFormatter, ["@my_email_formatter"]] - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container->register('my_mailer', ...); - $container->register('my_email_formatter', ...); - - $container - ->register('newsletter_manager', 'NewsletterManager') - ->addMethodCall('setMailer', array( - new Reference('my_mailer'), - )) - ->addMethodCall('setEmailFormatter', array( - new Reference('my_email_formatter'), - )) - ; - - $container - ->register('greeting_card_manager', 'GreetingCardManager') - ->addMethodCall('setMailer', array( - new Reference('my_mailer'), - )) - ->addMethodCall('setEmailFormatter', array( - new Reference('my_email_formatter'), - )) - ; - -There is a lot of repetition in both the classes and the configuration. This -means that if you changed, for example, the ``Mailer`` of ``EmailFormatter`` -classes to be injected via the constructor, you would need to update the config -in two places. Likewise if you needed to make changes to the setter methods -you would need to do this in both classes. The typical way to deal with the -common methods of these related classes would be to extract them to a super class:: - - abstract class MailManager - { - protected $mailer; - protected $emailFormatter; - - public function setMailer(Mailer $mailer) - { - $this->mailer = $mailer; - } - - public function setEmailFormatter(EmailFormatter $emailFormatter) - { - $this->emailFormatter = $emailFormatter; - } - - // ... - } - -The ``NewsletterManager`` and ``GreetingCardManager`` can then extend this -super class:: - - class NewsletterManager extends MailManager - { - // ... - } - -and:: - - class GreetingCardManager extends MailManager - { - // ... - } - -In a similar fashion, the Symfony service container also supports extending -services in the configuration so you can also reduce the repetition by specifying -a parent for a service. - -.. configuration-block:: - - .. code-block:: yaml - - # ... - services: - # ... - mail_manager: - abstract: true - calls: - - [setMailer, ["@my_mailer"]] - - [setEmailFormatter, ["@my_email_formatter"]] - - newsletter_manager: - class: "NewsletterManager" - parent: mail_manager - - greeting_card_manager: - class: "GreetingCardManager" - parent: mail_manager - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\DefinitionDecorator; - use Symfony\Component\DependencyInjection\Reference; - - // ... - $mailManager = new Definition(); - $mailManager - ->setAbstract(true); - ->addMethodCall('setMailer', array( - new Reference('my_mailer'), - )) - ->addMethodCall('setEmailFormatter', array( - new Reference('my_email_formatter'), - )) - ; - $container->setDefinition('mail_manager', $mailManager); - - $newsletterManager = new DefinitionDecorator('mail_manager'); - $newsletterManager->setClass('NewsletterManager'); - $container->setDefinition('newsletter_manager', $newsletterManager); - - $greetingCardManager = new DefinitionDecorator('mail_manager'); - $greetingCardManager->setClass('GreetingCardManager'); - $container->setDefinition('greeting_card_manager', $greetingCardManager); - -In this context, having a ``parent`` service implies that the arguments and -method calls of the parent service should be used for the child services. -Specifically, the setter methods defined for the parent service will be called -when the child services are instantiated. - -.. note:: - - If you remove the ``parent`` config key, the services will still be instantiated - and they will still of course extend the ``MailManager`` class. The difference - is that omitting the ``parent`` config key will mean that the ``calls`` - defined on the ``mail_manager`` service will not be executed when the - child services are instantiated. - -.. caution:: - - The ``scope``, ``abstract`` and ``tags`` attributes are always taken from - the child service. - -The parent service is abstract as it should not be directly retrieved from the -container or passed into another service. It exists merely as a "template" that -other services can use. This is why it can have no ``class`` configured which -would cause an exception to be raised for a non-abstract service. - -.. note:: - - In order for parent dependencies to resolve, the ``ContainerBuilder`` must - first be compiled. See :doc:`/components/dependency_injection/compilation` - for more details. - -.. tip:: - - In the examples shown, the classes sharing the same configuration also - extend from the same parent class in PHP. This isn't necessary at all. - You can just extract common parts of similar service definitions into - a parent service without also extending a parent class in PHP. - -Overriding parent Dependencies ------------------------------- - -There may be times where you want to override what class is passed in for -a dependency of one child service only. Fortunately, by adding the method -call config for the child service, the dependencies set by the parent class -will be overridden. So if you needed to pass a different dependency just -to the ``NewsletterManager`` class, the config would look like this: - -.. configuration-block:: - - .. code-block:: yaml - - # ... - services: - # ... - my_alternative_mailer: - # ... - - mail_manager: - abstract: true - calls: - - [setMailer, ["@my_mailer"]] - - [setEmailFormatter, ["@my_email_formatter"]] - - newsletter_manager: - class: "NewsletterManager" - parent: mail_manager - calls: - - [setMailer, ["@my_alternative_mailer"]] - - greeting_card_manager: - class: "GreetingCardManager" - parent: mail_manager - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\DefinitionDecorator; - use Symfony\Component\DependencyInjection\Reference; - - // ... - $container->setDefinition('my_alternative_mailer', ...); - - $mailManager = new Definition(); - $mailManager - ->setAbstract(true); - ->addMethodCall('setMailer', array( - new Reference('my_mailer'), - )) - ->addMethodCall('setEmailFormatter', array( - new Reference('my_email_formatter'), - )) - ; - $container->setDefinition('mail_manager', $mailManager); - - $newsletterManager = new DefinitionDecorator('mail_manager'); - $newsletterManager->setClass('NewsletterManager'); - ->addMethodCall('setMailer', array( - new Reference('my_alternative_mailer'), - )) - ; - $container->setDefinition('newsletter_manager', $newsletterManager); - - $greetingCardManager = new DefinitionDecorator('mail_manager'); - $greetingCardManager->setClass('GreetingCardManager'); - $container->setDefinition('greeting_card_manager', $greetingCardManager); - -The ``GreetingCardManager`` will receive the same dependencies as before, -but the ``NewsletterManager`` will be passed the ``my_alternative_mailer`` -instead of the ``my_mailer`` service. - -.. caution:: - - You can't override method calls. When you defined new method calls in the child - service, it'll be added to the current set of configured method calls. This means - it works perfectly when the setter overrides the current property, but it doesn't - work as expected when the setter appends it to the existing data (e.g. an - ``addFilters()`` method). - In those cases, the only solution is to *not* extend the parent service and configuring - the service just like you did before knowing this feature. diff --git a/components/dependency_injection/synthetic_services.rst b/components/dependency_injection/synthetic_services.rst deleted file mode 100644 index cbe32a8c60a..00000000000 --- a/components/dependency_injection/synthetic_services.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. index:: - single: DependencyInjection; Synthetic Services - -How to Inject Instances into the Container ------------------------------------------- - -When using the container in your application, you sometimes need to inject an -instance instead of configuring the container to create a new instance. - -For instance, if you're using the :doc:`HttpKernel ` -component with the DependencyInjection component, then the ``kernel`` -service is injected into the container from within the ``Kernel`` class:: - - // ... - abstract class Kernel implements KernelInterface, TerminableInterface - { - // ... - protected function initializeContainer() - { - // ... - $this->container->set('kernel', $this); - - // ... - } - } - -The ``kernel`` service is called a synthetic service. This service has to be -configured in the container, so the container knows the service does exist -during compilation (otherwise, services depending on this ``kernel`` service -will get a "service does not exists" error). - -In order to do so, you have to use -:method:`Definition::setSynthetic() `:: - - use Symfony\Component\DependencyInjectino\Definition; - - // synthetic services don't specify a class - $kernelDefinition = new Definition(); - $kernelDefinition->setSynthetic(true); - - $container->setDefinition('your_service', $kernelDefinition); - -Now, you can inject the instance in the container using -:method:`Container::set() `:: - - $yourService = new YourObject(); - $container->set('your_service', $yourService); - -``$container->get('your_service')`` will now return the same instance as -``$yourService``. diff --git a/components/dependency_injection/tags.rst b/components/dependency_injection/tags.rst deleted file mode 100644 index 9bb0d000707..00000000000 --- a/components/dependency_injection/tags.rst +++ /dev/null @@ -1,299 +0,0 @@ -.. index:: - single: DependencyInjection; Tags - -Working with Tagged Services -============================ - -Tags are a generic string (along with some options) that can be applied to -any service. By themselves, tags don't actually alter the functionality of your -services in any way. But if you choose to, you can ask a container builder -for a list of all services that were tagged with some specific tag. This -is useful in compiler passes where you can find these services and use or -modify them in some specific way. - -For example, if you are using Swift Mailer you might imagine that you want -to implement a "transport chain", which is a collection of classes implementing -``\Swift_Transport``. Using the chain, you'll want Swift Mailer to try several -ways of transporting the message until one succeeds. - -To begin with, define the ``TransportChain`` class:: - - class TransportChain - { - private $transports; - - public function __construct() - { - $this->transports = array(); - } - - public function addTransport(\Swift_Transport $transport) - { - $this->transports[] = $transport; - } - } - -Then, define the chain as a service: - -.. configuration-block:: - - .. code-block:: yaml - - services: - acme_mailer.transport_chain: - class: TransportChain - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $container->setDefinition('acme_mailer.transport_chain', new Definition('TransportChain')); - -Define Services with a custom Tag ---------------------------------- - -Now you might want several of the ``\Swift_Transport`` classes to be instantiated -and added to the chain automatically using the ``addTransport()`` method. -For example you may add the following transports as services: - -.. configuration-block:: - - .. code-block:: yaml - - services: - acme_mailer.transport.smtp: - class: \Swift_SmtpTransport - arguments: - - "%mailer_host%" - tags: - - { name: acme_mailer.transport } - acme_mailer.transport.sendmail: - class: \Swift_SendmailTransport - tags: - - { name: acme_mailer.transport } - - .. code-block:: xml - - - - - - - %mailer_host% - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%')); - $definitionSmtp->addTag('acme_mailer.transport'); - $container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp); - - $definitionSendmail = new Definition('\Swift_SendmailTransport'); - $definitionSendmail->addTag('acme_mailer.transport'); - $container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail); - -Notice that each was given a tag named ``acme_mailer.transport``. This is -the custom tag that you'll use in your compiler pass. The compiler pass -is what makes this tag "mean" something. - -Create a ``CompilerPass`` -------------------------- - -Your compiler pass can now ask the container for any services with the -custom tag:: - - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; - use Symfony\Component\DependencyInjection\Reference; - - class TransportCompilerPass implements CompilerPassInterface - { - public function process(ContainerBuilder $container) - { - if (!$container->has('acme_mailer.transport_chain')) { - return; - } - - $definition = $container->findDefinition( - 'acme_mailer.transport_chain' - ); - - $taggedServices = $container->findTaggedServiceIds( - 'acme_mailer.transport' - ); - foreach ($taggedServices as $id => $tags) { - $definition->addMethodCall( - 'addTransport', - array(new Reference($id)) - ); - } - } - } - -The ``process()`` method checks for the existence of the ``acme_mailer.transport_chain`` -service, then looks for all services tagged ``acme_mailer.transport``. It adds -to the definition of the ``acme_mailer.transport_chain`` service a call to -``addTransport()`` for each "acme_mailer.transport" service it has found. -The first argument of each of these calls will be the mailer transport service -itself. - -Register the Pass with the Container ------------------------------------- - -You also need to register the pass with the container, it will then be -run when the container is compiled:: - - use Symfony\Component\DependencyInjection\ContainerBuilder; - - $container = new ContainerBuilder(); - $container->addCompilerPass(new TransportCompilerPass()); - -.. note:: - - Compiler passes are registered differently if you are using the full-stack - framework. See :doc:`/cookbook/service_container/compiler_passes` for - more details. - -Adding additional Attributes on Tags ------------------------------------- - -Sometimes you need additional information about each service that's tagged with your tag. -For example, you might want to add an alias to each member of the transport chain. - -To begin with, change the ``TransportChain`` class:: - - class TransportChain - { - private $transports; - - public function __construct() - { - $this->transports = array(); - } - - public function addTransport(\Swift_Transport $transport, $alias) - { - $this->transports[$alias] = $transport; - } - - public function getTransport($alias) - { - if (array_key_exists($alias, $this->transports)) { - return $this->transports[$alias]; - } - } - } - -As you can see, when ``addTransport`` is called, it takes not only a ``Swift_Transport`` -object, but also a string alias for that transport. So, how can you allow -each tagged transport service to also supply an alias? - -To answer this, change the service declaration: - -.. configuration-block:: - - .. code-block:: yaml - - services: - acme_mailer.transport.smtp: - class: \Swift_SmtpTransport - arguments: - - "%mailer_host%" - tags: - - { name: acme_mailer.transport, alias: foo } - acme_mailer.transport.sendmail: - class: \Swift_SendmailTransport - tags: - - { name: acme_mailer.transport, alias: bar } - - .. code-block:: xml - - - - - - - %mailer_host% - - - - - - - - - - .. code-block:: php - - use Symfony\Component\DependencyInjection\Definition; - - $definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%')); - $definitionSmtp->addTag('acme_mailer.transport', array('alias' => 'foo')); - $container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp); - - $definitionSendmail = new Definition('\Swift_SendmailTransport'); - $definitionSendmail->addTag('acme_mailer.transport', array('alias' => 'bar')); - $container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail); - -Notice that you've added a generic ``alias`` key to the tag. To actually -use this, update the compiler:: - - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; - use Symfony\Component\DependencyInjection\Reference; - - class TransportCompilerPass implements CompilerPassInterface - { - public function process(ContainerBuilder $container) - { - if (!$container->hasDefinition('acme_mailer.transport_chain')) { - return; - } - - $definition = $container->getDefinition( - 'acme_mailer.transport_chain' - ); - - $taggedServices = $container->findTaggedServiceIds( - 'acme_mailer.transport' - ); - foreach ($taggedServices as $id => $tags) { - foreach ($tags as $attributes) { - $definition->addMethodCall( - 'addTransport', - array(new Reference($id), $attributes["alias"]) - ); - } - } - } - } - -The double loop may be confusing. This is because a service can have more than one -tag. You tag a service twice or more with the ``acme_mailer.transport`` tag. The -second foreach loop iterates over the ``acme_mailer.transport`` tags set for the -current service and gives you the attributes. diff --git a/components/dependency_injection/workflow.rst b/components/dependency_injection/workflow.rst index d528c10fa6b..5ad8ae7c3d8 100644 --- a/components/dependency_injection/workflow.rst +++ b/components/dependency_injection/workflow.rst @@ -4,30 +4,29 @@ Container Building Workflow =========================== -In the preceding pages of this section, there has been little to say about -where the various files and classes should be located. This is because this -depends on the application, library or framework in which you want to use -the container. Looking at how the container is configured and built in the -Symfony full-stack Framework will help you see how this all fits together, +The location of the files and classes related to the Dependency Injection +component depends on the application, library or framework in which you want +to use the container. Looking at how the container is configured and built +in the Symfony full-stack Framework will help you see how this all fits together, whether you are using the full-stack framework or looking to use the service container in another application. The full-stack framework uses the HttpKernel component to manage the loading -of the service container configuration from the application and bundles and -also handles the compilation and caching. Even if you are not using HttpKernel, -it should give you an idea of one way of organizing configuration in a modular -application. +of the service container configuration from the application and bundles +and also handles the compilation and caching. Even if you are not using +HttpKernel, it should give you an idea of one way of organizing configuration +in a modular application. Working with a Cached Container ------------------------------- -Before building it, the kernel checks to see if a cached version of the container -exists. The HttpKernel has a debug setting and if this is false, the -cached version is used if it exists. If debug is true then the kernel +Before building it, the kernel checks to see if a cached version of the +container exists. The HttpKernel has a debug setting and if this is false, +the cached version is used if it exists. If debug is true then the kernel :doc:`checks to see if configuration is fresh ` -and if it is, the cached version of the container is used. If not then the container -is built from the application-level configuration and the bundles's extension -configuration. +and if it is, the cached version of the container is used. If not then the +container is built from the application-level configuration and the bundles's +extension configuration. Read :ref:`Dumping the Configuration for Performance ` for more details. @@ -36,11 +35,13 @@ Application-level Configuration ------------------------------- Application level config is loaded from the ``app/config`` directory. Multiple -files are loaded which are then merged when the extensions are processed. This -allows for different configuration for different environments e.g. dev, prod. +files are loaded which are then merged when the extensions are processed. +This allows for different configuration for different environments e.g. +dev, prod. These files contain parameters and services that are loaded directly into -the container as per :ref:`Setting Up the Container with Configuration Files `. +the container as per +:ref:`Setting Up the Container with Configuration Files `. They also contain configuration that is processed by extensions as per :ref:`Managing Configuration with Extensions `. These are considered to be bundle configuration since each bundle contains @@ -51,28 +52,29 @@ Bundle-level Configuration with Extensions By convention, each bundle contains an Extension class which is in the bundle's ``DependencyInjection`` directory. These are registered with the ``ContainerBuilder`` -when the kernel is booted. When the ``ContainerBuilder`` is :doc:`compiled `, -the application-level configuration relevant to the bundle's extension is -passed to the Extension which also usually loads its own config file(s), typically from the bundle's -``Resources/config`` directory. The application-level config is usually processed -with a :doc:`Configuration object ` also stored -in the bundle's ``DependencyInjection`` directory. +when the kernel is booted. When the ``ContainerBuilder`` is +:doc:`compiled `, the application-level +configuration relevant to the bundle's extension is passed to the Extension +which also usually loads its own config file(s), typically from the bundle's +``Resources/config`` directory. The application-level config is usually +processed with a :doc:`Configuration object ` +also stored in the bundle's ``DependencyInjection`` directory. Compiler Passes to Allow Interaction between Bundles ---------------------------------------------------- -:ref:`Compiler passes ` are -used to allow interaction between different bundles as they cannot affect -each other's configuration in the extension classes. One of the main uses is -to process tagged services, allowing bundles to register services to be picked -up by other bundles, such as Monolog loggers, Twig extensions and Data Collectors -for the Web Profiler. Compiler passes are usually placed in the bundle's -``DependencyInjection/Compiler`` directory. +:ref:`Compiler passes ` +are used to allow interaction between different bundles as they cannot affect +each other's configuration in the extension classes. One of the main uses +is to process tagged services, allowing bundles to register services to +be picked up by other bundles, such as Monolog loggers, Twig extensions +and Data Collectors for the Web Profiler. Compiler passes are usually placed +in the bundle's ``DependencyInjection/Compiler`` directory. Compilation and Caching ----------------------- After the compilation process has loaded the services from the configuration, -extensions and the compiler passes, it is dumped so that the cache can be used -next time. The dumped version is then used during subsequent requests as it -is more efficient. +extensions and the compiler passes, it is dumped so that the cache can be +used next time. The dumped version is then used during subsequent requests +as it is more efficient. diff --git a/components/dom_crawler.rst b/components/dom_crawler.rst index 8de40e81772..cdf8d02643d 100644 --- a/components/dom_crawler.rst +++ b/components/dom_crawler.rst @@ -15,10 +15,11 @@ The DomCrawler Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/dom-crawler`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/DomCrawler). + $ composer require symfony/dom-crawler + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -28,9 +29,8 @@ Usage The :class:`Symfony\\Component\\DomCrawler\\Crawler` class provides methods to query and manipulate HTML and XML documents. -An instance of the Crawler represents a set (:phpclass:`SplObjectStorage`) -of :phpclass:`DOMElement` objects, which are basically nodes that you can -traverse easily:: +An instance of the Crawler represents a set of :phpclass:`DOMElement` objects, +which are basically nodes that you can traverse easily:: use Symfony\Component\DomCrawler\Crawler; @@ -80,7 +80,7 @@ This allows you to use jQuery-like selectors to traverse:: $crawler = $crawler->filter('body > p'); -Anonymous function can be used to filter with more complex criteria:: +An anonymous function can be used to filter with more complex criteria:: use Symfony\Component\DomCrawler\Crawler; // ... @@ -88,7 +88,7 @@ Anonymous function can be used to filter with more complex criteria:: $crawler = $crawler ->filter('body > p') ->reduce(function (Crawler $node, $i) { - // filter even nodes + // filters every other node return ($i % 2) == 0; }); @@ -197,7 +197,7 @@ Accessing Node Values Access the node name (HTML tag name) of the first node of the current selection (eg. "p" or "div"):: - // will return the node name (HTML tag name) of the first child element under + // returns the node name (HTML tag name) of the first child element under $tag = $crawler->filterXPath('//body/*')->nodeName(); Access the value of the first node of the current selection:: @@ -255,26 +255,24 @@ The crawler supports multiple ways of adding the content:: .. note:: When dealing with character sets other than ISO-8859-1, always add HTML - content using the :method:`Symfony\\Component\\DomCrawler\\Crawler::addHTMLContent` + content using the :method:`Symfony\\Component\\DomCrawler\\Crawler::addHtmlContent` method where you can specify the second parameter to be your target character set. As the Crawler's implementation is based on the DOM extension, it is also able to interact with native :phpclass:`DOMDocument`, :phpclass:`DOMNodeList` -and :phpclass:`DOMNode` objects: - -.. code-block:: php +and :phpclass:`DOMNode` objects:: - $document = new \DOMDocument(); - $document->loadXml(''); - $nodeList = $document->getElementsByTagName('node'); - $node = $document->getElementsByTagName('node')->item(0); + $domDocument = new \DOMDocument(); + $domDocument->loadXml(''); + $nodeList = $domDocument->getElementsByTagName('node'); + $node = $domDocument->getElementsByTagName('node')->item(0); - $crawler->addDocument($document); + $crawler->addDocument($domDocument); $crawler->addNodeList($nodeList); $crawler->addNodes(array($node)); $crawler->addNode($node); - $crawler->add($document); + $crawler->add($domDocument); .. _component-dom-crawler-dumping: @@ -299,19 +297,13 @@ and :phpclass:`DOMNode` objects: $html = $crawler->html(); - The ``html`` method is new in Symfony 2.3. - - .. caution:: - - Due to an issue in PHP, the ``html()`` method returns wrongly decoded HTML - entities in PHP versions lower than 5.3.6 (for example, it returns ``•`` - instead of ``•``). + The ``html()`` method is new in Symfony 2.3. Links ~~~~~ To find a link by name (or a clickable image by its ``alt`` attribute), use -the ``selectLink`` method on an existing crawler. This returns a Crawler +the ``selectLink()`` method on an existing crawler. This returns a Crawler instance with just the selected link(s). Calling ``link()`` gives you a special :class:`Symfony\\Component\\DomCrawler\\Link` object:: @@ -324,7 +316,7 @@ instance with just the selected link(s). Calling ``link()`` gives you a special The :class:`Symfony\\Component\\DomCrawler\\Link` object has several useful methods to get more information about the selected link itself:: - // return the proper URI that can be used to make another request + // returns the proper URI that can be used to make another request $uri = $link->getUri(); .. note:: @@ -345,10 +337,19 @@ given text. This method is especially useful because you can use it to return a :class:`Symfony\\Component\\DomCrawler\\Form` object that represents the form that the button lives in:: - $form = $crawler->selectButton('validate')->form(); + // button example: + + // you can get button by its label + $form = $crawler->selectButton('My super button')->form(); + + // or by button id (#my-super-button) if the button doesn't have a label + $form = $crawler->selectButton('my-super-button')->form(); + + // or you can filter the whole form, for example a form has a class attribute:
+ $crawler->filter('.form-vertical')->form(); // or "fill" the form fields with data - $form = $crawler->selectButton('validate')->form(array( + $form = $crawler->selectButton('my-super-button')->form(array( 'name' => 'Ryan', )); @@ -366,13 +367,13 @@ attribute followed by a query string of all of the form's values. You can virtually set and get values on the form:: - // set values on the form internally + // sets values on the form internally $form->setValues(array( 'registration[username]' => 'symfonyfan', 'registration[terms]' => 1, )); - // get back an array of values - in the "flat" array like above + // gets back an array of values - in the "flat" array like above $values = $form->getValues(); // returns the values like PHP would see them, @@ -389,13 +390,13 @@ To work with multi-dimensional fields:: Pass an array of values:: - // Set a single field + // sets a single field $form->setValues(array('multi' => array('value'))); - // Set multiple fields at once + // sets multiple fields at once $form->setValues(array('multi' => array( 1 => 'value', - 'dimensional' => 'an other value' + 'dimensional' => 'an other value', ))); This is great, but it gets better! The ``Form`` object allows you to interact @@ -404,17 +405,17 @@ and uploading files:: $form['registration[username]']->setValue('symfonyfan'); - // check or uncheck a checkbox + // checks or unchecks a checkbox $form['registration[terms]']->tick(); $form['registration[terms]']->untick(); - // select an option + // selects an option $form['registration[birthday][year]']->select(1984); - // select many options from a "multiple" select + // selects many options from a "multiple" select $form['registration[interests]']->select(array('symfony', 'cookies')); - // even fake a file upload + // fakes a file upload $form['registration[photo]']->upload('/path/to/lucas.jpg'); Using the Form Data @@ -443,16 +444,16 @@ directly:: use Goutte\Client; - // make a real request to an external site + // makes a real request to an external site $client = new Client(); $crawler = $client->request('GET', 'https://github.com/login'); // select the form and fill in some values - $form = $crawler->selectButton('Log in')->form(); + $form = $crawler->selectButton('Sign in')->form(); $form['login'] = 'symfonyfan'; $form['password'] = 'anypass'; - // submit that form + // submits the given form $crawler = $client->submit($form); .. _components-dom-crawler-invalid: @@ -465,12 +466,18 @@ to prevent you from setting invalid values. If you want to be able to set invalid values, you can use the ``disableValidation()`` method on either the whole form or specific field(s):: - // Disable validation for a specific field + // disables validation for a specific field $form['country']->disableValidation()->select('Invalid value'); - // Disable validation for the whole form + // disables validation for the whole form $form->disableValidation(); $form['country']->select('Invalid value'); -.. _`Goutte`: https://github.com/fabpot/goutte +.. _`Goutte`: https://github.com/FriendsOfPHP/Goutte .. _Packagist: https://packagist.org/packages/symfony/dom-crawler + +Learn more +---------- + +* :doc:`/testing` +* :doc:`/components/css_selector` diff --git a/components/event_dispatcher.rst b/components/event_dispatcher.rst new file mode 100644 index 00000000000..8f97727f737 --- /dev/null +++ b/components/event_dispatcher.rst @@ -0,0 +1,522 @@ +.. index:: + single: EventDispatcher + single: Components; EventDispatcher + +The EventDispatcher Component +============================= + + The EventDispatcher component provides tools that allow your application + components to communicate with each other by dispatching events and + listening to them. + +Introduction +------------ + +Object-oriented code has gone a long way to ensuring code extensibility. +By creating classes that have well defined responsibilities, your code becomes +more flexible and a developer can extend them with subclasses to modify +their behaviors. But if they want to share the changes with other developers +who have also made their own subclasses, code inheritance is no longer the +answer. + +Consider the real-world example where you want to provide a plugin system +for your project. A plugin should be able to add methods, or do something +before or after a method is executed, without interfering with other plugins. +This is not an easy problem to solve with single inheritance, and even if +multiple inheritance was possible with PHP, it comes with its own drawbacks. + +The Symfony EventDispatcher component implements the `Mediator`_ pattern +in a simple and effective way to make all these things possible and to make +your projects truly extensible. + +Take a simple example from :doc:`the HttpKernel component `. +Once a ``Response`` object has been created, it may be useful to allow other +elements in the system to modify it (e.g. add some cache headers) before +it's actually used. To make this possible, the Symfony kernel throws an +event - ``kernel.response``. Here's how it works: + +* A *listener* (PHP object) tells a central *dispatcher* object that it + wants to listen to the ``kernel.response`` event; + +* At some point, the Symfony kernel tells the *dispatcher* object to dispatch + the ``kernel.response`` event, passing with it an ``Event`` object that + has access to the ``Response`` object; + +* The dispatcher notifies (i.e. calls a method on) all listeners of the + ``kernel.response`` event, allowing each of them to make modifications + to the ``Response`` object. + +.. index:: + single: EventDispatcher; Events + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/event-dispatcher + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +Events +~~~~~~ + +When an event is dispatched, it's identified by a unique name (e.g. +``kernel.response``), which any number of listeners might be listening to. +An :class:`Symfony\\Component\\EventDispatcher\\Event` instance is also +created and passed to all of the listeners. As you'll see later, the ``Event`` +object itself often contains data about the event being dispatched. + +.. index:: + pair: EventDispatcher; Naming conventions + +Naming Conventions +.................. + +The unique event name can be any string, but optionally follows a few simple +naming conventions: + +* Use only lowercase letters, numbers, dots (``.``) and underscores (``_``); +* Prefix names with a namespace followed by a dot (e.g. ``order.``, ``user.*``); +* End names with a verb that indicates what action has been taken (e.g. + ``order.placed``). + +.. index:: + single: EventDispatcher; Event subclasses + +Event Names and Event Objects +............................. + +When the dispatcher notifies listeners, it passes an actual ``Event`` object +to those listeners. The base ``Event`` class is very simple: it +contains a method for stopping +:ref:`event propagation `, but not much +else. + +.. seealso:: + + Read ":doc:`/components/event_dispatcher/generic_event`" for more + information about this base event object. + +Often times, data about a specific event needs to be passed along with the +``Event`` object so that the listeners have the needed information. In such +case, a special subclass that has additional methods for retrieving and +overriding information can be passed when dispatching an event. For example, +the ``kernel.response`` event uses a +:class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent`, which +contains methods to get and even replace the ``Response`` object. + +The Dispatcher +~~~~~~~~~~~~~~ + +The dispatcher is the central object of the event dispatcher system. In +general, a single dispatcher is created, which maintains a registry of +listeners. When an event is dispatched via the dispatcher, it notifies all +listeners registered with that event:: + + use Symfony\Component\EventDispatcher\EventDispatcher; + + $dispatcher = new EventDispatcher(); + +.. index:: + single: EventDispatcher; Listeners + +Connecting Listeners +~~~~~~~~~~~~~~~~~~~~ + +To take advantage of an existing event, you need to connect a listener to +the dispatcher so that it can be notified when the event is dispatched. +A call to the dispatcher's ``addListener()`` method associates any valid +PHP callable to an event:: + + $listener = new AcmeListener(); + $dispatcher->addListener('acme.foo.action', array($listener, 'onFooAction')); + +The ``addListener()`` method takes up to three arguments: + +#. The event name (string) that this listener wants to listen to; +#. A PHP callable that will be executed when the specified event is dispatched; +#. An optional priority integer (higher equals more important and therefore + that the listener will be triggered earlier) that determines when a listener + is triggered versus other listeners (defaults to ``0``). If two listeners + have the same priority, they are executed in the order that they were + added to the dispatcher. + +.. note:: + + A `PHP callable`_ is a PHP variable that can be used by the + ``call_user_func()`` function and returns ``true`` when passed to the + ``is_callable()`` function. It can be a ``\Closure`` instance, an object + implementing an ``__invoke()`` method (which is what closures are in fact), + a string representing a function or an array representing an object + method or a class method. + + So far, you've seen how PHP objects can be registered as listeners. + You can also register PHP `Closures`_ as event listeners:: + + use Symfony\Component\EventDispatcher\Event; + + $dispatcher->addListener('acme.foo.action', function (Event $event) { + // will be executed when the acme.foo.action event is dispatched + }); + +Once a listener is registered with the dispatcher, it waits until the event +is notified. In the above example, when the ``acme.foo.action`` event is dispatched, +the dispatcher calls the ``AcmeListener::onFooAction()`` method and passes +the ``Event`` object as the single argument:: + + use Symfony\Component\EventDispatcher\Event; + + class AcmeListener + { + // ... + + public function onFooAction(Event $event) + { + // ... do something + } + } + +The ``$event`` argument is the event object that was passed when dispatching the +event. In many cases, a special event subclass is passed with extra +information. You can check the documentation or implementation of each event to +determine which instance is passed. + +.. sidebar:: Registering Event Listeners in the Service Container + + When you are using the + :class:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher` + and the + :doc:`DependencyInjection component `, + you can use the + :class:`Symfony\\Component\\EventDispatcher\\DependencyInjection\\RegisterListenersPass` + to tag services as event listeners:: + + use Symfony\Component\DependencyInjection\ContainerBuilder; + use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + use Symfony\Component\DependencyInjection\Reference; + use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; + use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; + + $containerBuilder = new ContainerBuilder(new ParameterBag()); + $containerBuilder->addCompilerPass(new RegisterListenersPass()); + + // registers the event dispatcher service + $containerBuilder->register('event_dispatcher', ContainerAwareEventDispatcher::class) + ->addArgument(new Reference('service_container')); + + // registers your event listener service + $containerBuilder->register('listener_service_id', \AcmeListener::class) + ->addTag('kernel.event_listener', array( + 'event' => 'acme.foo.action', + 'method' => 'onFooAction', + )); + + // registers an event subscriber + $containerBuilder->register('subscriber_service_id', \AcmeSubscriber::class) + ->addTag('kernel.event_subscriber'); + + By default, the listeners pass assumes that the event dispatcher's service + id is ``event_dispatcher``, that event listeners are tagged with the + ``kernel.event_listener`` tag and that event subscribers are tagged + with the ``kernel.event_subscriber`` tag. You can change these default + values by passing custom values to the constructor of ``RegisterListenersPass``. + +.. _event_dispatcher-closures-as-listeners: + +.. index:: + single: EventDispatcher; Creating and dispatching an event + +Creating and Dispatching an Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to registering listeners with existing events, you can create +and dispatch your own events. This is useful when creating third-party +libraries and also when you want to keep different components of your own +system flexible and decoupled. + +.. _creating-an-event-object: + +Creating an Event Class +....................... + +Suppose you want to create a new event - ``order.placed`` - that is dispatched +each time a customer orders a product with your application. When dispatching +this event, you'll pass a custom event instance that has access to the placed +order. Start by creating this custom event class and documenting it:: + + namespace Acme\Store\Event; + + use Symfony\Component\EventDispatcher\Event; + use Acme\Store\Order; + + /** + * The order.placed event is dispatched each time an order is created + * in the system. + */ + class OrderPlacedEvent extends Event + { + const NAME = 'order.placed'; + + protected $order; + + public function __construct(Order $order) + { + $this->order = $order; + } + + public function getOrder() + { + return $this->order; + } + } + +Each listener now has access to the order via the ``getOrder()`` method. + +.. note:: + + If you don't need to pass any additional data to the event listeners, you + can also use the default + :class:`Symfony\\Component\\EventDispatcher\\Event` class. In such case, + you can document the event and its name in a generic ``StoreEvents`` class, + similar to the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` + class. + +Dispatch the Event +.................. + +The :method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch` +method notifies all listeners of the given event. It takes two arguments: +the name of the event to dispatch and the ``Event`` instance to pass to +each listener of that event:: + + use Acme\Store\Order; + use Acme\Store\Event\OrderPlacedEvent; + + // the order is somehow created or retrieved + $order = new Order(); + // ... + + // creates the OrderPlacedEvent and dispatches it + $event = new OrderPlacedEvent($order); + $dispatcher->dispatch(OrderPlacedEvent::NAME, $event); + +Notice that the special ``OrderPlacedEvent`` object is created and passed to +the ``dispatch()`` method. Now, any listener to the ``order.placed`` +event will receive the ``OrderPlacedEvent``. + +.. index:: + single: EventDispatcher; Event subscribers + +.. _event_dispatcher-using-event-subscribers: + +Using Event Subscribers +~~~~~~~~~~~~~~~~~~~~~~~ + +The most common way to listen to an event is to register an *event listener* +with the dispatcher. This listener can listen to one or more events and +is notified each time those events are dispatched. + +Another way to listen to events is via an *event subscriber*. An event +subscriber is a PHP class that's able to tell the dispatcher exactly which +events it should subscribe to. It implements the +:class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface` +interface, which requires a single static method called +:method:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface::getSubscribedEvents`. +Take the following example of a subscriber that subscribes to the +``kernel.response`` and ``order.placed`` events:: + + namespace Acme\Store\Event; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + use Symfony\Component\HttpKernel\KernelEvents; + use Acme\Store\Event\OrderPlacedEvent; + + class StoreSubscriber implements EventSubscriberInterface + { + public static function getSubscribedEvents() + { + return array( + KernelEvents::RESPONSE => array( + array('onKernelResponsePre', 10), + array('onKernelResponsePost', -10), + ), + OrderPlacedEvent::NAME => 'onStoreOrder', + ); + } + + public function onKernelResponsePre(FilterResponseEvent $event) + { + // ... + } + + public function onKernelResponsePost(FilterResponseEvent $event) + { + // ... + } + + public function onStoreOrder(OrderPlacedEvent $event) + { + // ... + } + } + +This is very similar to a listener class, except that the class itself can +tell the dispatcher which events it should listen to. To register a subscriber +with the dispatcher, use the +:method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber` +method:: + + use Acme\Store\Event\StoreSubscriber; + // ... + + $subscriber = new StoreSubscriber(); + $dispatcher->addSubscriber($subscriber); + +The dispatcher will automatically register the subscriber for each event +returned by the ``getSubscribedEvents()`` method. This method returns an array +indexed by event names and whose values are either the method name to call +or an array composed of the method name to call and a priority. The example +above shows how to register several listener methods for the same event +in subscriber and also shows how to pass the priority of each listener method. +The higher the priority, the earlier the method is called. In the above +example, when the ``kernel.response`` event is triggered, the methods +``onKernelResponsePre()`` and ``onKernelResponsePost()`` are called in that +order. + +.. index:: + single: EventDispatcher; Stopping event flow + +.. _event_dispatcher-event-propagation: + +Stopping Event Flow/Propagation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In some cases, it may make sense for a listener to prevent any other listeners +from being called. In other words, the listener needs to be able to tell +the dispatcher to stop all propagation of the event to future listeners +(i.e. to not notify any more listeners). This can be accomplished from +inside a listener via the +:method:`Symfony\\Component\\EventDispatcher\\Event::stopPropagation` method:: + + use Acme\Store\Event\OrderPlacedEvent; + + public function onStoreOrder(OrderPlacedEvent $event) + { + // ... + + $event->stopPropagation(); + } + +Now, any listeners to ``order.placed`` that have not yet been called will +*not* be called. + +It is possible to detect if an event was stopped by using the +:method:`Symfony\\Component\\EventDispatcher\\Event::isPropagationStopped` +method which returns a boolean value:: + + // ... + $dispatcher->dispatch('foo.event', $event); + if ($event->isPropagationStopped()) { + // ... + } + +.. index:: + single: EventDispatcher; EventDispatcher aware events and listeners + +.. _event_dispatcher-dispatcher-aware-events: + +EventDispatcher Aware Events and Listeners +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``EventDispatcher`` always passes the dispatched event, the event's +name and a reference to itself to the listeners. This can lead to some advanced +applications of the ``EventDispatcher`` including dispatching other events inside +listeners, chaining events or even lazy loading listeners into the dispatcher object. + +.. index:: + single: EventDispatcher; Dispatcher shortcuts + +.. _event_dispatcher-shortcuts: + +Dispatcher Shortcuts +~~~~~~~~~~~~~~~~~~~~ + +If you do not need a custom event object, you can simply rely on a plain +:class:`Symfony\\Component\\EventDispatcher\\Event` object. You do not even +need to pass this to the dispatcher as it will create one by default unless you +specifically pass one:: + + $dispatcher->dispatch('order.placed'); + +Moreover, the event dispatcher always returns whichever event object that +was dispatched, i.e. either the event that was passed or the event that +was created internally by the dispatcher. This allows for nice shortcuts:: + + if (!$dispatcher->dispatch('foo.event')->isPropagationStopped()) { + // ... + } + +Or:: + + $event = new OrderPlacedEvent($order); + $order = $dispatcher->dispatch('bar.event', $event)->getOrder(); + +and so on. + +.. index:: + single: EventDispatcher; Event name introspection + +.. _event_dispatcher-event-name-introspection: + +Event Name Introspection +~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``EventDispatcher`` instance, as well as the name of the event that +is dispatched, are passed as arguments to the listener:: + + use Symfony\Component\EventDispatcher\Event; + use Symfony\Component\EventDispatcher\EventDispatcherInterface; + + class Foo + { + public function myEventListener(Event $event, $eventName, EventDispatcherInterface $dispatcher) + { + // ... do something with the event name + } + } + +Other Dispatchers +----------------- + +Besides the commonly used ``EventDispatcher``, the component comes +with some other dispatchers: + +* :doc:`/components/event_dispatcher/container_aware_dispatcher` +* :doc:`/components/event_dispatcher/immutable_dispatcher` +* :doc:`/components/event_dispatcher/traceable_dispatcher` (provided by the + :doc:`HttpKernel component `) + +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/event_dispatcher/* + /event_dispatcher/* + +* :ref:`The kernel.event_listener tag ` +* :ref:`The kernel.event_subscriber tag ` + +.. _Mediator: https://en.wikipedia.org/wiki/Mediator_pattern +.. _Closures: https://php.net/manual/en/functions.anonymous.php +.. _PHP callable: https://php.net/manual/en/language.pseudo-types.php#language.types.callback +.. _Packagist: https://packagist.org/packages/symfony/event-dispatcher diff --git a/components/event_dispatcher/container_aware_dispatcher.rst b/components/event_dispatcher/container_aware_dispatcher.rst index 82a502f582b..ce330df6be9 100644 --- a/components/event_dispatcher/container_aware_dispatcher.rst +++ b/components/event_dispatcher/container_aware_dispatcher.rst @@ -7,14 +7,15 @@ The Container Aware Event Dispatcher Introduction ------------ -The :class:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher` is -a special ``EventDispatcher`` implementation which is coupled to the service container -that is part of :doc:`the DependencyInjection component `. +The :class:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher` +is a special ``EventDispatcher`` implementation which is coupled to the +service container that is part of +:doc:`the DependencyInjection component `. It allows services to be specified as event listeners making the ``EventDispatcher`` extremely powerful. -Services are lazy loaded meaning the services attached as listeners will only be -created if an event is dispatched that requires those listeners. +Services are lazy loaded meaning the services attached as listeners will +only be created if an event is dispatched that requires those listeners. Setup ----- @@ -25,8 +26,8 @@ into the :class:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatc use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; - $container = new ContainerBuilder(); - $dispatcher = new ContainerAwareEventDispatcher($container); + $containerBuilder = new ContainerBuilder(); + $dispatcher = new ContainerAwareEventDispatcher($containerBuilder); Adding Listeners ---------------- @@ -34,8 +35,8 @@ Adding Listeners The ``ContainerAwareEventDispatcher`` can either load specified services directly or services that implement :class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface`. -The following examples assume the service container has been loaded with any -services that are mentioned. +The following examples assume the service container has been loaded with +any services that are mentioned. .. note:: @@ -67,6 +68,7 @@ and the second argument is the service's class name (which must implement The ``EventSubscriberInterface`` is exactly as you would expect:: use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\KernelEvents; // ... class StoreSubscriber implements EventSubscriberInterface @@ -74,7 +76,7 @@ The ``EventSubscriberInterface`` is exactly as you would expect:: public static function getSubscribedEvents() { return array( - 'kernel.response' => array( + KernelEvents::RESPONSE => array( array('onKernelResponsePre', 10), array('onKernelResponsePost', 0), ), diff --git a/components/event_dispatcher/generic_event.rst b/components/event_dispatcher/generic_event.rst index 27d3723e994..c26ee546432 100644 --- a/components/event_dispatcher/generic_event.rst +++ b/components/event_dispatcher/generic_event.rst @@ -4,20 +4,21 @@ The Generic Event Object ======================== -The base :class:`Symfony\\Component\\EventDispatcher\\Event` class provided by the -EventDispatcher component is deliberately sparse to allow the creation of -API specific event objects by inheritance using OOP. This allows for elegant and -readable code in complex applications. +The base :class:`Symfony\\Component\\EventDispatcher\\Event` class provided +by the EventDispatcher component is deliberately sparse to allow the creation +of API specific event objects by inheritance using OOP. This allows for +elegant and readable code in complex applications. The :class:`Symfony\\Component\\EventDispatcher\\GenericEvent` is available -for convenience for those who wish to use just one event object throughout their -application. It is suitable for most purposes straight out of the box, because -it follows the standard observer pattern where the event object +for convenience for those who wish to use just one event object throughout +their application. It is suitable for most purposes straight out of the +box, because it follows the standard observer pattern where the event object encapsulates an event 'subject', but has the addition of optional extra arguments. -:class:`Symfony\\Component\\EventDispatcher\\GenericEvent` has a simple API in -addition to the base class :class:`Symfony\\Component\\EventDispatcher\\Event` +:class:`Symfony\\Component\\EventDispatcher\\GenericEvent` has a simple +API in addition to the base class +:class:`Symfony\\Component\\EventDispatcher\\Event` * :method:`Symfony\\Component\\EventDispatcher\\GenericEvent::__construct`: Constructor takes the event subject and any arguments; @@ -41,8 +42,8 @@ addition to the base class :class:`Symfony\\Component\\EventDispatcher\\Event` Returns true if the argument key exists; The ``GenericEvent`` also implements :phpclass:`ArrayAccess` on the event -arguments which makes it very convenient to pass extra arguments regarding the -event subject. +arguments which makes it very convenient to pass extra arguments regarding +the event subject. The following examples show use-cases to give a general idea of the flexibility. The examples assume event listeners have been added to the dispatcher. @@ -64,8 +65,8 @@ Simply passing a subject:: } } -Passing and processing arguments using the :phpclass:`ArrayAccess` API to access -the event arguments:: +Passing and processing arguments using the :phpclass:`ArrayAccess` API to +access the event arguments:: use Symfony\Component\EventDispatcher\GenericEvent; diff --git a/components/event_dispatcher/immutable_dispatcher.rst b/components/event_dispatcher/immutable_dispatcher.rst index 0732f7597dd..905d1db5b01 100644 --- a/components/event_dispatcher/immutable_dispatcher.rst +++ b/components/event_dispatcher/immutable_dispatcher.rst @@ -4,13 +4,13 @@ The Immutable Event Dispatcher ============================== -The :class:`Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher` is -a locked or frozen event dispatcher. The dispatcher cannot register new +The :class:`Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher` +is a locked or frozen event dispatcher. The dispatcher cannot register new listeners or subscribers. -The ``ImmutableEventDispatcher`` takes another event dispatcher with all the -listeners and subscribers. The immutable dispatcher is just a proxy of this -original dispatcher. +The ``ImmutableEventDispatcher`` takes another event dispatcher with all +the listeners and subscribers. The immutable dispatcher is just a proxy +of this original dispatcher. To use it, first create a normal dispatcher (``EventDispatcher`` or ``ContainerAwareEventDispatcher``) and register some listeners or @@ -35,4 +35,4 @@ Now, inject that into an ``ImmutableEventDispatcher``:: You'll need to use this new dispatcher in your project. If you are trying to execute one of the methods which modifies the dispatcher -(e.g. ``addListener``), a ``BadMethodCallException`` is thrown. +(e.g. ``addListener()``), a ``BadMethodCallException`` is thrown. diff --git a/components/event_dispatcher/index.rst b/components/event_dispatcher/index.rst deleted file mode 100644 index 27cd9155cd4..00000000000 --- a/components/event_dispatcher/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -EventDispatcher -=============== - -.. toctree:: - :maxdepth: 2 - - introduction - container_aware_dispatcher - generic_event - immutable_dispatcher - traceable_dispatcher diff --git a/components/event_dispatcher/introduction.rst b/components/event_dispatcher/introduction.rst deleted file mode 100644 index b784bbe470a..00000000000 --- a/components/event_dispatcher/introduction.rst +++ /dev/null @@ -1,657 +0,0 @@ -.. index:: - single: EventDispatcher - single: Components; EventDispatcher - -The EventDispatcher Component -============================= - - The EventDispatcher component provides tools that allow your application - components to communicate with each other by dispatching events and listening - to them. - -Introduction ------------- - -Object-oriented code has gone a long way to ensuring code extensibility. By -creating classes that have well defined responsibilities, your code becomes -more flexible and a developer can extend them with subclasses to modify their -behaviors. But if they want to share the changes with other developers who have -also made their own subclasses, code inheritance is no longer the answer. - -Consider the real-world example where you want to provide a plugin system for -your project. A plugin should be able to add methods, or do something before -or after a method is executed, without interfering with other plugins. This is -not an easy problem to solve with single inheritance, and multiple inheritance -(were it possible with PHP) has its own drawbacks. - -The Symfony EventDispatcher component implements the `Mediator`_ pattern in -a simple and effective way to make all these things possible and to make your -projects truly extensible. - -Take a simple example from :doc:`/components/http_kernel/introduction`. Once a -``Response`` object has been created, it may be useful to allow other elements -in the system to modify it (e.g. add some cache headers) before it's actually -used. To make this possible, the Symfony kernel throws an event - -``kernel.response``. Here's how it works: - -* A *listener* (PHP object) tells a central *dispatcher* object that it wants - to listen to the ``kernel.response`` event; - -* At some point, the Symfony kernel tells the *dispatcher* object to dispatch - the ``kernel.response`` event, passing with it an ``Event`` object that has - access to the ``Response`` object; - -* The dispatcher notifies (i.e. calls a method on) all listeners of the - ``kernel.response`` event, allowing each of them to make modifications to - the ``Response`` object. - -.. index:: - single: EventDispatcher; Events - -Installation ------------- - -You can install the component in 2 different ways: - -* :doc:`Install it via Composer ` (``symfony/event-dispatcher`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/EventDispatcher). - -.. include:: /components/require_autoload.rst.inc - -Usage ------ - -Events -~~~~~~ - -When an event is dispatched, it's identified by a unique name (e.g. -``kernel.response``), which any number of listeners might be listening to. An -:class:`Symfony\\Component\\EventDispatcher\\Event` instance is also created -and passed to all of the listeners. As you'll see later, the ``Event`` object -itself often contains data about the event being dispatched. - -.. index:: - pair: EventDispatcher; Naming conventions - -Naming Conventions -.................. - -The unique event name can be any string, but optionally follows a few simple -naming conventions: - -* use only lowercase letters, numbers, dots (``.``), and underscores (``_``); - -* prefix names with a namespace followed by a dot (e.g. ``kernel.``); - -* end names with a verb that indicates what action is being taken (e.g. - ``request``). - -Here are some examples of good event names: - -* ``kernel.response`` -* ``form.pre_set_data`` - -.. index:: - single: EventDispatcher; Event subclasses - -Event Names and Event Objects -............................. - -When the dispatcher notifies listeners, it passes an actual ``Event`` object -to those listeners. The base ``Event`` class is very simple: it contains a -method for stopping :ref:`event -propagation `, but not much else. - -Often times, data about a specific event needs to be passed along with the -``Event`` object so that the listeners have needed information. In the case of -the ``kernel.response`` event, the ``Event`` object that's created and passed to -each listener is actually of type -:class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent`, a -subclass of the base ``Event`` object. This class contains methods such as -``getResponse`` and ``setResponse``, allowing listeners to get or even replace -the ``Response`` object. - -The moral of the story is this: When creating a listener to an event, the -``Event`` object that's passed to the listener may be a special subclass that -has additional methods for retrieving information from and responding to the -event. - -The Dispatcher -~~~~~~~~~~~~~~ - -The dispatcher is the central object of the event dispatcher system. In -general, a single dispatcher is created, which maintains a registry of -listeners. When an event is dispatched via the dispatcher, it notifies all -listeners registered with that event:: - - use Symfony\Component\EventDispatcher\EventDispatcher; - - $dispatcher = new EventDispatcher(); - -.. index:: - single: EventDispatcher; Listeners - -Connecting Listeners -~~~~~~~~~~~~~~~~~~~~ - -To take advantage of an existing event, you need to connect a listener to the -dispatcher so that it can be notified when the event is dispatched. A call to -the dispatcher's ``addListener()`` method associates any valid PHP callable to -an event:: - - $listener = new AcmeListener(); - $dispatcher->addListener('foo.action', array($listener, 'onFooAction')); - -The ``addListener()`` method takes up to three arguments: - -* The event name (string) that this listener wants to listen to; - -* A PHP callable that will be notified when an event is thrown that it listens - to; - -* An optional priority integer (higher equals more important, and therefore - that the listener will be triggered earlier) that determines when a listener - is triggered versus other listeners (defaults to ``0``). If two listeners - have the same priority, they are executed in the order that they were added - to the dispatcher. - -.. note:: - - A `PHP callable`_ is a PHP variable that can be used by the - ``call_user_func()`` function and returns ``true`` when passed to the - ``is_callable()`` function. It can be a ``\Closure`` instance, an object - implementing an ``__invoke`` method (which is what closures are in fact), - a string representing a function, or an array representing an object - method or a class method. - - So far, you've seen how PHP objects can be registered as listeners. You - can also register PHP `Closures`_ as event listeners:: - - use Symfony\Component\EventDispatcher\Event; - - $dispatcher->addListener('foo.action', function (Event $event) { - // will be executed when the foo.action event is dispatched - }); - -Once a listener is registered with the dispatcher, it waits until the event is -notified. In the above example, when the ``foo.action`` event is dispatched, -the dispatcher calls the ``AcmeListener::onFooAction`` method and passes the -``Event`` object as the single argument:: - - use Symfony\Component\EventDispatcher\Event; - - class AcmeListener - { - // ... - - public function onFooAction(Event $event) - { - // ... do something - } - } - -In many cases, a special ``Event`` subclass that's specific to the given event -is passed to the listener. This gives the listener access to special -information about the event. Check the documentation or implementation of each -event to determine the exact ``Symfony\Component\EventDispatcher\Event`` -instance that's being passed. For example, the ``kernel.response`` event passes an -instance of ``Symfony\Component\HttpKernel\Event\FilterResponseEvent``:: - - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; - - public function onKernelResponse(FilterResponseEvent $event) - { - $response = $event->getResponse(); - $request = $event->getRequest(); - - // ... - } - -.. sidebar:: Registering Event Listeners in the Service Container - - When you are using the - :class:`Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher` - and the - :doc:`DependencyInjection component `, - you can use the - :class:`Symfony\\Component\\EventDispatcher\\DependencyInjection\\RegisterListenersPass` - to tag services as event listeners:: - - use Symfony\Component\DependencyInjection\ContainerBuilder; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; - use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; - - $containerBuilder = new ContainerBuilder(new ParameterBag()); - $containerBuilder->addCompilerPass(new RegisterListenersPass()); - - // register the event dispatcher service - $containerBuilder->setDefinition('event_dispatcher', new Definition( - 'Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher', - array(new Reference('service_container')) - )); - - // register your event listener service - $listener = new Definition('AcmeListener'); - $listener->addTag('kernel.event_listener', array( - 'event' => 'foo.action', - 'method' => 'onFooAction', - )); - $containerBuilder->setDefinition('listener_service_id', $listener); - - // register an event subscriber - $subscriber = new Definition('AcmeSubscriber'); - $subscriber->addTag('kernel.event_subscriber'); - $containerBuilder->setDefinition('subscriber_service_id', $subscriber); - - By default, the listeners pass assumes that the event dispatcher's service - id is ``event_dispatcher``, that event listeners are tagged with the - ``kernel.event_listener`` tag and that event subscribers are tagged with - the ``kernel.event_subscriber`` tag. You can change these default values - by passing custom values to the constructor of ``RegisterListenersPass``. - -.. _event_dispatcher-closures-as-listeners: - -.. index:: - single: EventDispatcher; Creating and dispatching an event - -Creating and Dispatching an Event -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In addition to registering listeners with existing events, you can create and -dispatch your own events. This is useful when creating third-party libraries -and also when you want to keep different components of your own system -flexible and decoupled. - -The Static ``Events`` Class -........................... - -Suppose you want to create a new Event - ``store.order`` - that is dispatched -each time an order is created inside your application. To keep things -organized, start by creating a ``StoreEvents`` class inside your application -that serves to define and document your event:: - - namespace Acme\StoreBundle; - - final class StoreEvents - { - /** - * The store.order event is thrown each time an order is created - * in the system. - * - * The event listener receives an - * Acme\StoreBundle\Event\FilterOrderEvent instance. - * - * @var string - */ - const STORE_ORDER = 'store.order'; - } - -Notice that this class doesn't actually *do* anything. The purpose of the -``StoreEvents`` class is just to be a location where information about common -events can be centralized. Notice also that a special ``FilterOrderEvent`` -class will be passed to each listener of this event. - -Creating an Event Object -........................ - -Later, when you dispatch this new event, you'll create an ``Event`` instance -and pass it to the dispatcher. The dispatcher then passes this same instance -to each of the listeners of the event. If you don't need to pass any -information to your listeners, you can use the default -``Symfony\Component\EventDispatcher\Event`` class. Most of the time, however, -you *will* need to pass information about the event to each listener. To -accomplish this, you'll create a new class that extends -``Symfony\Component\EventDispatcher\Event``. - -In this example, each listener will need access to some pretend ``Order`` -object. Create an ``Event`` class that makes this possible:: - - namespace Acme\StoreBundle\Event; - - use Symfony\Component\EventDispatcher\Event; - use Acme\StoreBundle\Order; - - class FilterOrderEvent extends Event - { - protected $order; - - public function __construct(Order $order) - { - $this->order = $order; - } - - public function getOrder() - { - return $this->order; - } - } - -Each listener now has access to the ``Order`` object via the ``getOrder`` -method. - -Dispatch the Event -.................. - -The :method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::dispatch` -method notifies all listeners of the given event. It takes two arguments: the -name of the event to dispatch and the ``Event`` instance to pass to each -listener of that event:: - - use Acme\StoreBundle\StoreEvents; - use Acme\StoreBundle\Order; - use Acme\StoreBundle\Event\FilterOrderEvent; - - // the order is somehow created or retrieved - $order = new Order(); - // ... - - // create the FilterOrderEvent and dispatch it - $event = new FilterOrderEvent($order); - $dispatcher->dispatch(StoreEvents::STORE_ORDER, $event); - -Notice that the special ``FilterOrderEvent`` object is created and passed to -the ``dispatch`` method. Now, any listener to the ``store.order`` event will -receive the ``FilterOrderEvent`` and have access to the ``Order`` object via -the ``getOrder`` method:: - - // some listener class that's been registered for "store.order" event - use Acme\StoreBundle\Event\FilterOrderEvent; - - public function onStoreOrder(FilterOrderEvent $event) - { - $order = $event->getOrder(); - // do something to or with the order - } - -.. index:: - single: EventDispatcher; Event subscribers - -.. _event_dispatcher-using-event-subscribers: - -Using Event Subscribers -~~~~~~~~~~~~~~~~~~~~~~~ - -The most common way to listen to an event is to register an *event listener* -with the dispatcher. This listener can listen to one or more events and is -notified each time those events are dispatched. - -Another way to listen to events is via an *event subscriber*. An event -subscriber is a PHP class that's able to tell the dispatcher exactly which -events it should subscribe to. It implements the -:class:`Symfony\\Component\\EventDispatcher\\EventSubscriberInterface` -interface, which requires a single static method called -``getSubscribedEvents``. Take the following example of a subscriber that -subscribes to the ``kernel.response`` and ``store.order`` events:: - - namespace Acme\StoreBundle\Event; - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\FilterResponseEvent; - - class StoreSubscriber implements EventSubscriberInterface - { - public static function getSubscribedEvents() - { - return array( - 'kernel.response' => array( - array('onKernelResponsePre', 10), - array('onKernelResponseMid', 5), - array('onKernelResponsePost', 0), - ), - 'store.order' => array('onStoreOrder', 0), - ); - } - - public function onKernelResponsePre(FilterResponseEvent $event) - { - // ... - } - - public function onKernelResponseMid(FilterResponseEvent $event) - { - // ... - } - - public function onKernelResponsePost(FilterResponseEvent $event) - { - // ... - } - - public function onStoreOrder(FilterOrderEvent $event) - { - // ... - } - } - -This is very similar to a listener class, except that the class itself can -tell the dispatcher which events it should listen to. To register a subscriber -with the dispatcher, use the -:method:`Symfony\\Component\\EventDispatcher\\EventDispatcher::addSubscriber` -method:: - - use Acme\StoreBundle\Event\StoreSubscriber; - - $subscriber = new StoreSubscriber(); - $dispatcher->addSubscriber($subscriber); - -The dispatcher will automatically register the subscriber for each event -returned by the ``getSubscribedEvents`` method. This method returns an array -indexed by event names and whose values are either the method name to call or -an array composed of the method name to call and a priority. The example -above shows how to register several listener methods for the same event in -subscriber and also shows how to pass the priority of each listener method. -The higher the priority, the earlier the method is called. In the above -example, when the ``kernel.response`` event is triggered, the methods -``onKernelResponsePre``, ``onKernelResponseMid``, and ``onKernelResponsePost`` -are called in that order. - -.. index:: - single: EventDispatcher; Stopping event flow - -.. _event_dispatcher-event-propagation: - -Stopping Event Flow/Propagation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In some cases, it may make sense for a listener to prevent any other listeners -from being called. In other words, the listener needs to be able to tell the -dispatcher to stop all propagation of the event to future listeners (i.e. to -not notify any more listeners). This can be accomplished from inside a -listener via the -:method:`Symfony\\Component\\EventDispatcher\\Event::stopPropagation` method:: - - use Acme\StoreBundle\Event\FilterOrderEvent; - - public function onStoreOrder(FilterOrderEvent $event) - { - // ... - - $event->stopPropagation(); - } - -Now, any listeners to ``store.order`` that have not yet been called will *not* -be called. - -It is possible to detect if an event was stopped by using the -:method:`Symfony\\Component\\EventDispatcher\\Event::isPropagationStopped` method -which returns a boolean value:: - - $dispatcher->dispatch('foo.event', $event); - if ($event->isPropagationStopped()) { - // ... - } - -.. index:: - single: EventDispatcher; EventDispatcher aware events and listeners - -.. _event_dispatcher-dispatcher-aware-events: - -EventDispatcher aware Events and Listeners -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``EventDispatcher`` always passes the dispatched event, the event's name -and a reference to itself to the listeners. This can be used in some advanced -usages of the ``EventDispatcher`` like dispatching other events in listeners, -event chaining or even lazy loading of more listeners into the dispatcher -object as shown in the following examples. - -Lazy loading listeners:: - - use Symfony\Component\EventDispatcher\Event; - use Symfony\Component\EventDispatcher\EventDispatcherInterface; - use Acme\StoreBundle\Event\StoreSubscriber; - - class Foo - { - private $started = false; - - public function myLazyListener( - Event $event, - $eventName, - EventDispatcherInterface $dispatcher - ) { - if (false === $this->started) { - $subscriber = new StoreSubscriber(); - $dispatcher->addSubscriber($subscriber); - } - - $this->started = true; - - // ... more code - } - } - -Dispatching another event from within a listener:: - - use Symfony\Component\EventDispatcher\Event; - use Symfony\Component\EventDispatcher\EventDispatcherInterface; - - class Foo - { - public function myFooListener( - Event $event, - $eventName, - EventDispatcherInterface $dispatcher - ) { - $dispatcher->dispatch('log', $event); - - // ... more code - } - } - -While this above is sufficient for most uses, if your application uses multiple -``EventDispatcher`` instances, you might need to specifically inject a known -instance of the ``EventDispatcher`` into your listeners. This could be done -using constructor or setter injection as follows: - -Constructor injection:: - - use Symfony\Component\EventDispatcher\EventDispatcherInterface; - - class Foo - { - protected $dispatcher = null; - - public function __construct(EventDispatcherInterface $dispatcher) - { - $this->dispatcher = $dispatcher; - } - } - -Or setter injection:: - - use Symfony\Component\EventDispatcher\EventDispatcherInterface; - - class Foo - { - protected $dispatcher = null; - - public function setEventDispatcher(EventDispatcherInterface $dispatcher) - { - $this->dispatcher = $dispatcher; - } - } - -Choosing between the two is really a matter of taste. Many tend to prefer the -constructor injection as the objects are fully initialized at construction -time. But when you have a long list of dependencies, using setter injection -can be the way to go, especially for optional dependencies. - -.. index:: - single: EventDispatcher; Dispatcher shortcuts - -.. _event_dispatcher-shortcuts: - -Dispatcher Shortcuts -~~~~~~~~~~~~~~~~~~~~ - -The :method:`EventDispatcher::dispatch ` -method always returns an :class:`Symfony\\Component\\EventDispatcher\\Event` -object. This allows for various shortcuts. For example, if one does not need -a custom event object, one can simply rely on a plain -:class:`Symfony\\Component\\EventDispatcher\\Event` object. You do not even need -to pass this to the dispatcher as it will create one by default unless you -specifically pass one:: - - $dispatcher->dispatch('foo.event'); - -Moreover, the event dispatcher always returns whichever event object that -was dispatched, i.e. either the event that was passed or the event that was -created internally by the dispatcher. This allows for nice shortcuts:: - - if (!$dispatcher->dispatch('foo.event')->isPropagationStopped()) { - // ... - } - -Or:: - - $barEvent = new BarEvent(); - $bar = $dispatcher->dispatch('bar.event', $barEvent)->getBar(); - -Or:: - - $bar = $dispatcher->dispatch('bar.event', new BarEvent())->getBar(); - -and so on... - -.. index:: - single: EventDispatcher; Event name introspection - -.. _event_dispatcher-event-name-introspection: - -Event Name Introspection -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 2.4 - Before Symfony 2.4, the event name and the event dispatcher had to be - requested from the ``Event`` instance. These methods are now deprecated. - -The ``EventDispatcher`` instance, as well as the name of the event that is -dispatched, are passed as arguments to the listener:: - - use Symfony\Component\EventDispatcher\Event; - use Symfony\Component\EventDispatcher\EventDispatcherInterface; - - class Foo - { - public function myEventListener(Event $event, $eventName, EventDispatcherInterface $dispatcher) - { - // ... do something with the event name - } - } - -Other Dispatchers ------------------ - -Besides the commonly used ``EventDispatcher``, the component comes with 2 -other dispatchers: - -* :doc:`/components/event_dispatcher/container_aware_dispatcher` -* :doc:`/components/event_dispatcher/immutable_dispatcher` - -.. _Mediator: http://en.wikipedia.org/wiki/Mediator_pattern -.. _Closures: http://php.net/manual/en/functions.anonymous.php -.. _PHP callable: http://www.php.net/manual/en/language.pseudo-types.php#language.types.callback -.. _Packagist: https://packagist.org/packages/symfony/event-dispatcher diff --git a/components/event_dispatcher/traceable_dispatcher.rst b/components/event_dispatcher/traceable_dispatcher.rst index d023d429ae5..57f05ba6e0d 100644 --- a/components/event_dispatcher/traceable_dispatcher.rst +++ b/components/event_dispatcher/traceable_dispatcher.rst @@ -15,10 +15,10 @@ Pass the event dispatcher to be wrapped and an instance of the use Symfony\Component\Stopwatch\Stopwatch; // the event dispatcher to debug - $eventDispatcher = ...; + $dispatcher = ...; $traceableEventDispatcher = new TraceableEventDispatcher( - $eventDispatcher, + $dispatcher, new Stopwatch() ); @@ -27,7 +27,7 @@ to register event listeners and dispatch events:: // ... - // register an event listener + // registers an event listener $eventListener = ...; $priority = ...; $traceableEventDispatcher->addListener( @@ -36,14 +36,14 @@ to register event listeners and dispatch events:: $priority ); - // dispatch an event + // dispatches an event $event = ...; $traceableEventDispatcher->dispatch('event.the_name', $event); After your application has been processed, you can use the :method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getCalledListeners` -method to retrieve an array of event listeners that have been called in your -application. Similarly, the +method to retrieve an array of event listeners that have been called in +your application. Similarly, the :method:`Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcherInterface::getNotCalledListeners` method returns an array of event listeners that have not been called:: diff --git a/components/expression_language/introduction.rst b/components/expression_language.rst similarity index 81% rename from components/expression_language/introduction.rst rename to components/expression_language.rst index 98b4402f7c7..9a55f3ed066 100644 --- a/components/expression_language/introduction.rst +++ b/components/expression_language.rst @@ -12,16 +12,19 @@ The ExpressionLanguage Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/expression-language`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/expression-language). + $ composer require symfony/expression-language + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc How can the Expression Engine Help Me? -------------------------------------- The purpose of the component is to allow users to use expressions inside -configuration for more complex logic. For some examples, the Symfony2 Framework +configuration for more complex logic. For some examples, the Symfony Framework uses expressions in security, for validation rules and in route matching. Besides using the component in the framework itself, the ExpressionLanguage @@ -66,11 +69,11 @@ The main class of the component is use Symfony\Component\ExpressionLanguage\ExpressionLanguage; - $language = new ExpressionLanguage(); + $expressionLanguage = new ExpressionLanguage(); - var_dump($language->evaluate('1 + 2')); // displays 3 + var_dump($expressionLanguage->evaluate('1 + 2')); // displays 3 - var_dump($language->compile('1 + 2')); // displays (1 + 2) + var_dump($expressionLanguage->compile('1 + 2')); // displays (1 + 2) Expression Syntax ----------------- @@ -86,7 +89,7 @@ PHP type (including objects):: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; - $language = new ExpressionLanguage(); + $expressionLanguage = new ExpressionLanguage(); class Apple { @@ -96,7 +99,7 @@ PHP type (including objects):: $apple = new Apple(); $apple->variety = 'Honeycrisp'; - var_dump($language->evaluate( + var_dump($expressionLanguage->evaluate( 'fruit.variety', array( 'fruit' => $apple, @@ -112,4 +115,15 @@ Caching The component provides some different caching strategies, read more about them in :doc:`/components/expression_language/caching`. +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/expression_language/* + /service_container/expression_language + /reference/constraints/Expression + .. _Packagist: https://packagist.org/packages/symfony/expression-language diff --git a/components/expression_language/caching.rst b/components/expression_language/caching.rst index 9c60ecc67bf..dbb5a1310b3 100644 --- a/components/expression_language/caching.rst +++ b/components/expression_language/caching.rst @@ -36,8 +36,8 @@ in the object using the constructor:: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Acme\ExpressionLanguage\ParserCache\MyDatabaseParserCache; - $cache = new MyDatabaseParserCache(...); - $language = new ExpressionLanguage($cache); + $databaseParserCache = new MyDatabaseParserCache(...); + $expressionLanguage = new ExpressionLanguage($databaseParserCache); .. note:: @@ -54,9 +54,9 @@ Both ``evaluate()`` and ``compile()`` can handle ``ParsedExpression`` and // ... // the parse() method returns a ParsedExpression - $expression = $language->parse('1 + 4', array()); + $expression = $expressionLanguage->parse('1 + 4', array()); - var_dump($language->evaluate($expression)); // prints 5 + var_dump($expressionLanguage->evaluate($expression)); // prints 5 .. code-block:: php @@ -65,10 +65,10 @@ Both ``evaluate()`` and ``compile()`` can handle ``ParsedExpression`` and $expression = new SerializedParsedExpression( '1 + 4', - serialize($language->parse('1 + 4', array())->getNodes()) + serialize($expressionLanguage->parse('1 + 4', array())->getNodes()) ); - var_dump($language->evaluate($expression)); // prints 5 + var_dump($expressionLanguage->evaluate($expression)); // prints 5 -.. _DoctrineBridge: https://github.com/symfony/DoctrineBridge +.. _DoctrineBridge: https://github.com/symfony/doctrine-bridge .. _`doctrine cache library`: http://docs.doctrine-project.org/projects/doctrine-common/en/latest/reference/caching.html diff --git a/components/expression_language/extending.rst b/components/expression_language/extending.rst index fa90165014d..4c766c4cddc 100644 --- a/components/expression_language/extending.rst +++ b/components/expression_language/extending.rst @@ -33,8 +33,8 @@ This method has 3 arguments: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; - $language = new ExpressionLanguage(); - $language->register('lowercase', function ($str) { + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->register('lowercase', function ($str) { return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str); }, function ($arguments, $str) { if (!is_string($str)) { @@ -44,7 +44,7 @@ This method has 3 arguments: return strtolower($str); }); - var_dump($language->evaluate('lowercase("HELLO")')); + var_dump($expressionLanguage->evaluate('lowercase("HELLO")')); This will print ``hello``. Both the **compiler** and **evaluator** are passed an ``arguments`` variable as their first argument, which is equal to the @@ -64,13 +64,11 @@ to add custom functions. To do so, you can create a new expression provider by creating a class that implements :class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface`. -This interface requires one method: +This interface requires one method: :method:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface::getFunctions`, which returns an array of expression functions (instances of :class:`Symfony\\Component\\ExpressionLanguage\\ExpressionFunction`) to -register. - -.. code-block:: php +register:: use Symfony\Component\ExpressionLanguage\ExpressionFunction; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; @@ -100,13 +98,13 @@ or by using the second argument of the constructor:: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; // using the constructor - $language = new ExpressionLanguage(null, array( + $expressionLanguage = new ExpressionLanguage(null, array( new StringExpressionLanguageProvider(), // ... )); // using registerProvider() - $language->registerProvider(new StringExpressionLanguageProvider()); + $expressionLanguage->registerProvider(new StringExpressionLanguageProvider()); .. tip:: @@ -120,7 +118,7 @@ or by using the second argument of the constructor:: { public function __construct(ParserCacheInterface $parser = null, array $providers = array()) { - // prepend the default provider to let users override it easily + // prepends the default provider to let users override it easily array_unshift($providers, new StringExpressionLanguageProvider()); parent::__construct($parser, $providers); diff --git a/components/expression_language/index.rst b/components/expression_language/index.rst deleted file mode 100644 index aa63907d921..00000000000 --- a/components/expression_language/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Expression Language -=================== - -.. toctree:: - :maxdepth: 2 - - introduction - syntax - extending - caching diff --git a/components/expression_language/syntax.rst b/components/expression_language/syntax.rst index ba153905aa6..dae2c8c517a 100644 --- a/components/expression_language/syntax.rst +++ b/components/expression_language/syntax.rst @@ -20,6 +20,18 @@ The component supports: * **booleans** - ``true`` and ``false`` * **null** - ``null`` +.. caution:: + + A backslash (``\``) must be escaped by 4 backslashes (``\\\\``) in a string + and 8 backslashes (``\\\\\\\\``) in a regex:: + + echo $expressionLanguage->evaluate('"\\\\"'); // prints \ + $expressionLanguage->evaluate('"a\\\\b" matches "/^a\\\\\\\\b$/"'); // returns true + + Control characters (e.g. ``\n``) in expressions are replaced with + whitespace. To avoid this, escape the sequence with a single backslash + (e.g. ``\\n``). + .. _component-expression-objects: Working with Objects @@ -42,7 +54,7 @@ to JavaScript:: $apple = new Apple(); $apple->variety = 'Honeycrisp'; - var_dump($language->evaluate( + var_dump($expressionLanguage->evaluate( 'fruit.variety', array( 'fruit' => $apple, @@ -72,7 +84,7 @@ JavaScript:: $robot = new Robot(); - var_dump($language->evaluate( + var_dump($expressionLanguage->evaluate( 'robot.sayHi(3)', array( 'robot' => $robot, @@ -93,7 +105,7 @@ constant:: define('DB_USER', 'root'); - var_dump($language->evaluate( + var_dump($expressionLanguage->evaluate( 'constant("DB_USER")' )); @@ -114,7 +126,7 @@ array keys, similar to JavaScript:: $data = array('life' => 10, 'universe' => 10, 'everything' => 22); - var_dump($language->evaluate( + var_dump($expressionLanguage->evaluate( 'data["life"] + data["universe"] + data["everything"]', array( 'data' => $data, @@ -140,7 +152,7 @@ Arithmetic Operators For example:: - var_dump($language->evaluate( + var_dump($expressionLanguage->evaluate( 'life + universe + everything', array( 'life' => 10, @@ -176,14 +188,14 @@ Comparison Operators To test if a string does *not* match a regex, use the logical ``not`` operator in combination with the ``matches`` operator:: - $language->evaluate('not ("foo" matches "/bar/")'); // returns true + $expressionLanguage->evaluate('not ("foo" matches "/bar/")'); // returns true You must use parenthesis because the unary operator ``not`` has precedence over the binary operator ``matches``. Examples:: - $ret1 = $language->evaluate( + $ret1 = $expressionLanguage->evaluate( 'life == everything', array( 'life' => 10, @@ -192,7 +204,7 @@ Examples:: ) ); - $ret2 = $language->evaluate( + $ret2 = $expressionLanguage->evaluate( 'life > everything', array( 'life' => 10, @@ -212,7 +224,7 @@ Logical Operators For example:: - $ret = $language->evaluate( + $ret = $expressionLanguage->evaluate( 'life < universe or life < everything', array( 'life' => 10, @@ -230,7 +242,7 @@ String Operators For example:: - var_dump($language->evaluate( + var_dump($expressionLanguage->evaluate( 'firstName~" "~lastName', array( 'firstName' => 'Arthur', @@ -256,7 +268,7 @@ For example:: $user = new User(); $user->group = 'human_resources'; - $inGroup = $language->evaluate( + $inGroup = $expressionLanguage->evaluate( 'user.group in ["human_resources", "marketing"]', array( 'user' => $user, @@ -280,7 +292,7 @@ For example:: $user = new User(); $user->age = 34; - $language->evaluate( + $expressionLanguage->evaluate( 'user.age in 18..45', array( 'user' => $user, diff --git a/components/filesystem.rst b/components/filesystem.rst new file mode 100644 index 00000000000..410b505cd0c --- /dev/null +++ b/components/filesystem.rst @@ -0,0 +1,294 @@ +.. index:: + single: Filesystem + +The Filesystem Component +======================== + + The Filesystem component provides basic utilities for the filesystem. + +.. tip:: + + The lock handler feature was introduced in symfony 2.6. + :doc:`See the documentation for more information `. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/filesystem + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The :class:`Symfony\\Component\\Filesystem\\Filesystem` class is the unique +endpoint for filesystem operations:: + + use Symfony\Component\Filesystem\Filesystem; + use Symfony\Component\Filesystem\Exception\IOExceptionInterface; + + $fileSystem = new Filesystem(); + + try { + $fileSystem->mkdir('/tmp/random/dir/'.mt_rand()); + } catch (IOExceptionInterface $exception) { + echo "An error occurred while creating your directory at ".$exception->getPath(); + } + +.. note:: + + Methods :method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir`, + :method:`Symfony\\Component\\Filesystem\\Filesystem::exists`, + :method:`Symfony\\Component\\Filesystem\\Filesystem::touch`, + :method:`Symfony\\Component\\Filesystem\\Filesystem::remove`, + :method:`Symfony\\Component\\Filesystem\\Filesystem::chmod`, + :method:`Symfony\\Component\\Filesystem\\Filesystem::chown` and + :method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` can receive a + string, an array or any object implementing :phpclass:`Traversable` as + the target argument. + +mkdir +~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir` creates a directory recursively. +On POSIX filesystems, directories are created with a default mode value +`0777`. You can use the second argument to set your own mode:: + + $fileSystem->mkdir('/tmp/photos', 0700); + +.. note:: + + You can pass an array or any :phpclass:`Traversable` object as the first + argument. + +.. note:: + + This function ignores already existing directories. + +.. note:: + + The directory permissions are affected by the current `umask`_. + Set the umask for your webserver, use PHP's :phpfunction:`umask` + function or use the :phpfunction:`chmod` function after the + directory has been created. + +exists +~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::exists` checks for the +presence of one or more files or directories and returns ``false`` if any of +them is missing:: + + // if this absolute directory exists, returns true + $fileSystem->exists('/tmp/photos'); + + // if rabbit.jpg exists and bottle.png does not exist, returns false + // non-absolute paths are relative to the directory where the running PHP script is stored + $fileSystem->exists(array('rabbit.jpg', 'bottle.png')); + +.. note:: + + You can pass an array or any :phpclass:`Traversable` object as the first + argument. + +copy +~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::copy` makes a copy of a +single file (use :method:`Symfony\\Component\\Filesystem\\Filesystem::mirror` to +copy directories). If the target already exists, the file is copied only if the +source modification date is later than the target. This behavior can be overridden +by the third boolean argument:: + + // works only if image-ICC has been modified after image.jpg + $fileSystem->copy('image-ICC.jpg', 'image.jpg'); + + // image.jpg will be overridden + $fileSystem->copy('image-ICC.jpg', 'image.jpg', true); + +touch +~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::touch` sets access and +modification time for a file. The current time is used by default. You can set +your own with the second argument. The third argument is the access time:: + + // sets modification time to the current timestamp + $fileSystem->touch('file.txt'); + // sets modification time 10 seconds in the future + $fileSystem->touch('file.txt', time() + 10); + // sets access time 10 seconds in the past + $fileSystem->touch('file.txt', time(), time() - 10); + +.. note:: + + You can pass an array or any :phpclass:`Traversable` object as the first + argument. + +chown +~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::chown` changes the owner of +a file. The third argument is a boolean recursive option:: + + // sets the owner of the lolcat video to www-data + $fileSystem->chown('lolcat.mp4', 'www-data'); + // changes the owner of the video directory recursively + $fileSystem->chown('/video', 'www-data', true); + +.. note:: + + You can pass an array or any :phpclass:`Traversable` object as the first + argument. + +chgrp +~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` changes the group of +a file. The third argument is a boolean recursive option:: + + // sets the group of the lolcat video to nginx + $fileSystem->chgrp('lolcat.mp4', 'nginx'); + // changes the group of the video directory recursively + $fileSystem->chgrp('/video', 'nginx', true); + +.. note:: + + You can pass an array or any :phpclass:`Traversable` object as the first + argument. + +chmod +~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::chmod` changes the mode or +permissions of a file. The fourth argument is a boolean recursive option:: + + // sets the mode of the video to 0600 + $fileSystem->chmod('video.ogg', 0600); + // changes the mod of the src directory recursively + $fileSystem->chmod('src', 0700, 0000, true); + +.. note:: + + You can pass an array or any :phpclass:`Traversable` object as the first + argument. + +remove +~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::remove` deletes files, +directories and symlinks:: + + $fileSystem->remove(array('symlink', '/path/to/directory', 'activity.log')); + +.. note:: + + You can pass an array or any :phpclass:`Traversable` object as the first + argument. + +rename +~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::rename` changes the name +of a single file or directory:: + + // renames a file + $fileSystem->rename('/tmp/processed_video.ogg', '/path/to/store/video_647.ogg'); + // renames a directory + $fileSystem->rename('/tmp/files', '/path/to/store/files'); + +symlink +~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::symlink` creates a +symbolic link from the target to the destination. If the filesystem does not +support symbolic links, a third boolean argument is available:: + + // creates a symbolic link + $fileSystem->symlink('/path/to/source', '/path/to/destination'); + // duplicates the source directory if the filesystem + // does not support symbolic links + $fileSystem->symlink('/path/to/source', '/path/to/destination', true); + +makePathRelative +~~~~~~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::makePathRelative` takes two +absolute paths and returns the relative path from the second path to the first one:: + + // returns '../' + $fileSystem->makePathRelative( + '/var/lib/symfony/src/Symfony/', + '/var/lib/symfony/src/Symfony/Component' + ); + // returns 'videos/' + $fileSystem->makePathRelative('/tmp/videos', '/tmp') + +mirror +~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::mirror` copies all the +contents of the source directory into the target one (use the +:method:`Symfony\\Component\\Filesystem\\Filesystem::copy` method to copy single +files):: + + $fileSystem->mirror('/path/to/source', '/path/to/target'); + +isAbsolutePath +~~~~~~~~~~~~~~ + +:method:`Symfony\\Component\\Filesystem\\Filesystem::isAbsolutePath` returns +``true`` if the given path is absolute, ``false`` otherwise:: + + // returns true + $fileSystem->isAbsolutePath('/tmp'); + // returns true + $fileSystem->isAbsolutePath('c:\\Windows'); + // returns false + $fileSystem->isAbsolutePath('tmp'); + // returns false + $fileSystem->isAbsolutePath('../dir'); + +dumpFile +~~~~~~~~ + +.. versionadded:: 2.3 + The ``dumpFile()`` was introduced in Symfony 2.3. + +:method:`Symfony\\Component\\Filesystem\\Filesystem::dumpFile` saves the given +contents into a file. It does this in an atomic manner: it writes a temporary +file first and then moves it to the new file location when it's finished. +This means that the user will always see either the complete old file or +complete new file (but never a partially-written file):: + + $fileSystem->dumpFile('file.txt', 'Hello World'); + +The ``file.txt`` file contains ``Hello World`` now. + +Error Handling +-------------- + +Whenever something wrong happens, an exception implementing +:class:`Symfony\\Component\\Filesystem\\Exception\\ExceptionInterface` or +:class:`Symfony\\Component\\Filesystem\\Exception\\IOExceptionInterface` is thrown. + +.. note:: + + An :class:`Symfony\\Component\\Filesystem\\Exception\\IOException` is + thrown if directory creation fails. + +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + filesystem/* + +.. _`Packagist`: https://packagist.org/packages/symfony/filesystem +.. _`umask`: https://en.wikipedia.org/wiki/Umask diff --git a/components/filesystem/index.rst b/components/filesystem/index.rst deleted file mode 100644 index e643f5a90f4..00000000000 --- a/components/filesystem/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Filesystem -========== - -.. toctree:: - :maxdepth: 2 - - introduction - lock_handler diff --git a/components/filesystem/introduction.rst b/components/filesystem/introduction.rst deleted file mode 100644 index 7e872390df5..00000000000 --- a/components/filesystem/introduction.rst +++ /dev/null @@ -1,269 +0,0 @@ -.. index:: - single: Filesystem - -The Filesystem Component -======================== - - The Filesystem component provides basic utilities for the filesystem. - -.. tip:: - - The lock handler feature was introduced in symfony 2.6. - :doc:`See the documentation for more information `. - -Installation ------------- - -You can install the component in 2 different ways: - -* :doc:`Install it via Composer ` (``symfony/filesystem`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Filesystem). - -.. include:: /components/require_autoload.rst.inc - -Usage ------ - -The :class:`Symfony\\Component\\Filesystem\\Filesystem` class is the unique -endpoint for filesystem operations:: - - use Symfony\Component\Filesystem\Filesystem; - use Symfony\Component\Filesystem\Exception\IOExceptionInterface; - - $fs = new Filesystem(); - - try { - $fs->mkdir('/tmp/random/dir/'.mt_rand()); - } catch (IOExceptionInterface $e) { - echo "An error occurred while creating your directory at ".$e->getPath(); - } - -.. note:: - - Methods :method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::exists`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::touch`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::remove`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::chmod`, - :method:`Symfony\\Component\\Filesystem\\Filesystem::chown` and - :method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` can receive a - string, an array or any object implementing :phpclass:`Traversable` as - the target argument. - -mkdir -~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::mkdir` creates a directory. -On POSIX filesystems, directories are created with a default mode value -`0777`. You can use the second argument to set your own mode:: - - $fs->mkdir('/tmp/photos', 0700); - -.. note:: - - You can pass an array or any :phpclass:`Traversable` object as the first - argument. - -exists -~~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::exists` checks for the -presence of all files or directories and returns ``false`` if a file is missing:: - - // this directory exists, return true - $fs->exists('/tmp/photos'); - - // rabbit.jpg exists, bottle.png does not exists, return false - $fs->exists(array('rabbit.jpg', 'bottle.png')); - -.. note:: - - You can pass an array or any :phpclass:`Traversable` object as the first - argument. - -copy -~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::copy` is used to copy -files. If the target already exists, the file is copied only if the source -modification date is later than the target. This behavior can be overridden by -the third boolean argument:: - - // works only if image-ICC has been modified after image.jpg - $fs->copy('image-ICC.jpg', 'image.jpg'); - - // image.jpg will be overridden - $fs->copy('image-ICC.jpg', 'image.jpg', true); - -touch -~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::touch` sets access and -modification time for a file. The current time is used by default. You can set -your own with the second argument. The third argument is the access time:: - - // set modification time to the current timestamp - $fs->touch('file.txt'); - // set modification time 10 seconds in the future - $fs->touch('file.txt', time() + 10); - // set access time 10 seconds in the past - $fs->touch('file.txt', time(), time() - 10); - -.. note:: - - You can pass an array or any :phpclass:`Traversable` object as the first - argument. - -chown -~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::chown` is used to change -the owner of a file. The third argument is a boolean recursive option:: - - // set the owner of the lolcat video to www-data - $fs->chown('lolcat.mp4', 'www-data'); - // change the owner of the video directory recursively - $fs->chown('/video', 'www-data', true); - -.. note:: - - You can pass an array or any :phpclass:`Traversable` object as the first - argument. - -chgrp -~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::chgrp` is used to change -the group of a file. The third argument is a boolean recursive option:: - - // set the group of the lolcat video to nginx - $fs->chgrp('lolcat.mp4', 'nginx'); - // change the group of the video directory recursively - $fs->chgrp('/video', 'nginx', true); - -.. note:: - - You can pass an array or any :phpclass:`Traversable` object as the first - argument. - -chmod -~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::chmod` is used to change -the mode of a file. The fourth argument is a boolean recursive option:: - - // set the mode of the video to 0600 - $fs->chmod('video.ogg', 0600); - // change the mod of the src directory recursively - $fs->chmod('src', 0700, 0000, true); - -.. note:: - - You can pass an array or any :phpclass:`Traversable` object as the first - argument. - -remove -~~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::remove` is used to remove -files, symlinks, directories easily:: - - $fs->remove(array('symlink', '/path/to/directory', 'activity.log')); - -.. note:: - - You can pass an array or any :phpclass:`Traversable` object as the first - argument. - -rename -~~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::rename` is used to rename -files and directories:: - - // rename a file - $fs->rename('/tmp/processed_video.ogg', '/path/to/store/video_647.ogg'); - // rename a directory - $fs->rename('/tmp/files', '/path/to/store/files'); - -symlink -~~~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::symlink` creates a -symbolic link from the target to the destination. If the filesystem does not -support symbolic links, a third boolean argument is available:: - - // create a symbolic link - $fs->symlink('/path/to/source', '/path/to/destination'); - // duplicate the source directory if the filesystem - // does not support symbolic links - $fs->symlink('/path/to/source', '/path/to/destination', true); - -makePathRelative -~~~~~~~~~~~~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::makePathRelative` returns -the relative path of a directory given another one:: - - // returns '../' - $fs->makePathRelative( - '/var/lib/symfony/src/Symfony/', - '/var/lib/symfony/src/Symfony/Component' - ); - // returns 'videos/' - $fs->makePathRelative('/tmp/videos', '/tmp') - -mirror -~~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::mirror` mirrors a -directory:: - - $fs->mirror('/path/to/source', '/path/to/target'); - -isAbsolutePath -~~~~~~~~~~~~~~ - -:method:`Symfony\\Component\\Filesystem\\Filesystem::isAbsolutePath` returns -``true`` if the given path is absolute, ``false`` otherwise:: - - // return true - $fs->isAbsolutePath('/tmp'); - // return true - $fs->isAbsolutePath('c:\\Windows'); - // return false - $fs->isAbsolutePath('tmp'); - // return false - $fs->isAbsolutePath('../dir'); - -dumpFile -~~~~~~~~ - -.. versionadded:: 2.3 - The ``dumpFile()`` was introduced in Symfony 2.3. - -:method:`Symfony\\Component\\Filesystem\\Filesystem::dumpFile` allows you to -dump contents to a file. It does this in an atomic manner: it writes a temporary -file first and then moves it to the new file location when it's finished. -This means that the user will always see either the complete old file or -complete new file (but never a partially-written file):: - - $fs->dumpFile('file.txt', 'Hello World'); - -The ``file.txt`` file contains ``Hello World`` now. - -A desired file mode can be passed as the third argument. - -Error Handling --------------- - -Whenever something wrong happens, an exception implementing -:class:`Symfony\\Component\\Filesystem\\Exception\\ExceptionInterface` or -:class:`Symfony\\Component\\Filesystem\\Exception\\IOExceptionInterface` is thrown. - -.. note:: - - An :class:`Symfony\\Component\\Filesystem\\Exception\\IOException` is - thrown if directory creation fails. - -.. _`Packagist`: https://packagist.org/packages/symfony/filesystem diff --git a/components/filesystem/lock_handler.rst b/components/filesystem/lock_handler.rst index 5d442e79547..0639dfee6a6 100644 --- a/components/filesystem/lock_handler.rst +++ b/components/filesystem/lock_handler.rst @@ -22,9 +22,7 @@ Usage The lock handler only works if you're using just one server. If you have several hosts, you must not use this helper. -A lock can be used, for example, to allow only one instance of a command to run. - -.. code-block:: php +A lock can be used, for example, to allow only one instance of a command to run:: use Symfony\Component\Filesystem\LockHandler; @@ -48,13 +46,23 @@ the file, so you can pass any value for this argument. to avoid name collisions, ``LockHandler`` also appends a hash to the name of the lock file. -By default, the lock will be created in the temporary directory, but you can -optionally select the directory where locks are created by passing it as the -second argument of the constructor. +By default, the lock will be created in the system's temporary directory, but +you can optionally select the directory where locks are created by passing it as +the second argument of the constructor. + +.. tip:: + + Another way to configure the directory where the locks are created is to + define a special environment variable, because PHP will use that value to + override the default temporary directory. On Unix-based systems, define the + ``TMPDIR`` variable. On Windows systems, define any of these variables: + ``TMP``, ``TEMP`` or ``USERPROFILE`` (they are checked in this order). This + technique is useful for example when deploying a third-party Symfony + application whose code can't be modified. The :method:`Symfony\\Component\\Filesystem\\LockHandler::lock` method tries to acquire the lock. If the lock is acquired, the method returns ``true``, -``false`` otherwise. If the ``lock`` method is called several times on the same +``false`` otherwise. If the ``lock()`` method is called several times on the same instance it will always return ``true`` if the lock was acquired on the first call. diff --git a/components/finder.rst b/components/finder.rst index 09babd0809c..ed6fbf507b6 100644 --- a/components/finder.rst +++ b/components/finder.rst @@ -5,16 +5,17 @@ The Finder Component ==================== - The Finder component finds files and directories via an intuitive fluent - interface. + The Finder component finds files and directories via an intuitive fluent + interface. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/finder`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Finder). + $ composer require symfony/finder + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -30,18 +31,18 @@ directories:: $finder->files()->in(__DIR__); foreach ($finder as $file) { - // Dump the absolute path - var_dump($file->getRealpath()); + // dumps the absolute path + var_dump($file->getRealPath()); - // Dump the relative path to the file, omitting the filename + // dumps the relative path to the file, omitting the filename var_dump($file->getRelativePath()); - // Dump the relative path to the file + // dumps the relative path to the file var_dump($file->getRelativePathname()); } The ``$file`` is an instance of :class:`Symfony\\Component\\Finder\\SplFileInfo` -which extends :phpclass:`SplFileInfo` to provide methods to work with relative +which extends PHP's own :phpclass:`SplFileInfo` to provide methods to work with relative paths. The above code prints the names of all the files in the current directory @@ -50,11 +51,17 @@ the Finder instance. .. tip:: - A Finder instance is a PHP :phpclass:`Iterator`. So, instead of iterating over the + A Finder instance is a PHP :phpclass:`Iterator`. So, in addition to iterating over the Finder with ``foreach``, you can also convert it to an array with the :phpfunction:`iterator_to_array` method, or get the number of items with :phpfunction:`iterator_count`. +.. caution:: + + The ``Finder`` object doesn't reset its internal state automatically. + This means that you need to create a new instance if you do not want + get mixed results. + .. caution:: When searching through multiple locations passed to the @@ -82,7 +89,11 @@ directory to use for the search:: Search in several locations by chaining calls to :method:`Symfony\\Component\\Finder\\Finder::in`:: - $finder->files()->in(__DIR__)->in('/elsewhere'); + // search inside *both* directories + $finder->in(array(__DIR__, '/elsewhere')); + + // same as above + $finder->in(__DIR__)->in('/elsewhere'); Use wildcard characters to search in the directories matching a pattern:: @@ -93,6 +104,7 @@ Each pattern has to resolve to at least one directory path. Exclude directories from matching with the :method:`Symfony\\Component\\Finder\\Finder::exclude` method:: + // directories passed as argument must be relative to the ones defined with the in() method $finder->in(__DIR__)->exclude('ruby'); .. versionadded:: 2.3 @@ -106,6 +118,10 @@ It's also possible to ignore directories that you don't have permission to read: As the Finder uses PHP iterators, you can pass any URL with a supported `protocol`_:: + // always add a trailing slash when looking for in the FTP root dir + $finder->in('ftp://example.com/'); + + // you can also look for in a FTP directory $finder->in('ftp://example.com/pub/'); And it also works with user-defined streams:: @@ -161,12 +177,9 @@ Sort the result by name or by type (directories first, then files):: You can also define your own sorting algorithm with ``sort()`` method:: - $sort = function (\SplFileInfo $a, \SplFileInfo $b) - { - return strcmp($a->getRealpath(), $b->getRealpath()); - }; - - $finder->sort($sort); + $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b) { + return strcmp($a->getRealPath(), $b->getRealPath()); + }); File Name ~~~~~~~~~ @@ -206,7 +219,10 @@ Path Restrict files and directories by path with the :method:`Symfony\\Component\\Finder\\Finder::path` method:: - $finder->path('some/special/dir'); + // matches files that contain "data" anywhere in their paths (files or directories) + $finder->path('data'); + // for example this will match data/*.xml and data.xml if they exist + $finder->path('data')->name('*.xml'); On all platforms slash (i.e. ``/``) should be used as the directory separator. @@ -306,8 +322,8 @@ The contents of returned files can be read with // ... } -.. _strtotime: http://www.php.net/manual/en/datetime.formats.php -.. _protocol: http://www.php.net/manual/en/wrappers.php -.. _Streams: http://www.php.net/streams -.. _IEC standard: http://physics.nist.gov/cuu/Units/binary.html +.. _strtotime: https://php.net/manual/en/datetime.formats.php +.. _protocol: https://php.net/manual/en/wrappers.php +.. _Streams: https://php.net/streams +.. _IEC standard: https://physics.nist.gov/cuu/Units/binary.html .. _Packagist: https://packagist.org/packages/symfony/finder diff --git a/components/form/introduction.rst b/components/form.rst similarity index 71% rename from components/form/introduction.rst rename to components/form.rst index 91dce8944c4..6a7e81956f8 100644 --- a/components/form/introduction.rst +++ b/components/form.rst @@ -5,8 +5,7 @@ The Form Component ================== - The Form component allows you to easily create, process and reuse HTML - forms. + The Form component allows you to easily create, process and reuse forms. The Form component is a tool to help you solve the problem of allowing end-users to interact with the data and modify the data in your application. And though @@ -17,10 +16,11 @@ be from a normal form post or from an API. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/form`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Form). + $ composer require symfony/form + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -54,7 +54,7 @@ support for very important features: The Symfony Form component relies on other libraries to solve these problems. Most of the time you will use Twig and the Symfony -:doc:`HttpFoundation `, +:doc:`HttpFoundation `, Translation and Validator components, but you can replace any of these with a different library of your choice. @@ -63,7 +63,7 @@ factory. .. tip:: - For a working example, see https://github.com/bschussek/standalone-forms + For a working example, see https://github.com/webmozart/standalone-forms Request Handling ~~~~~~~~~~~~~~~~ @@ -84,7 +84,7 @@ object to read data off of the correct PHP superglobals (i.e. ``$_POST`` or If you need more control over exactly when your form is submitted or which data is passed to it, you can use the :method:`Symfony\\Component\\Form\\FormInterface::submit` - for this. Read more about it :ref:`in the cookbook `. + for this. Read more about it :ref:`form-call-submit-directly`. .. sidebar:: Integration with the HttpFoundation Component @@ -107,51 +107,54 @@ object to read data off of the correct PHP superglobals (i.e. ``$_POST`` or .. note:: For more information about the HttpFoundation component or how to - install it, see :doc:`/components/http_foundation/introduction`. + install it, see :doc:`/components/http_foundation`. CSRF Protection ~~~~~~~~~~~~~~~ Protection against CSRF attacks is built into the Form component, but you need -to explicitly enable it or replace it with a custom solution. The following -snippet adds CSRF protection to the form factory:: +to explicitly enable it or replace it with a custom solution. If you want to +use the built-in support, first install the Security CSRF component: + +.. code-block:: terminal + + $ composer require symfony/security-csrf + +The following snippet adds CSRF protection to the form factory:: use Symfony\Component\Form\Forms; - use Symfony\Component\Form\Extension\Csrf\CsrfExtension; - use Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider; use Symfony\Component\HttpFoundation\Session\Session; + use Symfony\Component\Form\Extension\Csrf\CsrfExtension; + use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; + use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; + use Symfony\Component\Security\Csrf\CsrfTokenManager; - // generate a CSRF secret from somewhere - $csrfSecret = ''; - - // create a Session object from the HttpFoundation component + // creates a Session object from the HttpFoundation component $session = new Session(); - $csrfProvider = new SessionCsrfProvider($session, $csrfSecret); + $csrfGenerator = new UriSafeTokenGenerator(); + $csrfStorage = new SessionTokenStorage($session); + $csrfManager = new CsrfTokenManager($csrfGenerator, $csrfStorage); $formFactory = Forms::createFormFactoryBuilder() // ... - ->addExtension(new CsrfExtension($csrfProvider)) + ->addExtension(new CsrfExtension($csrfManager)) ->getFormFactory(); -To secure your application against CSRF attacks, you need to define a CSRF -secret. Generate a random string with at least 32 characters, insert it in the -above snippet and make sure that nobody except your web server can access -the secret. - Internally, this extension will automatically add a hidden field to every -form (called ``_token`` by default) whose value is automatically generated -and validated when binding the form. +form (called ``_token`` by default) whose value is automatically generated by +the CSRF generator and validated when binding the form. .. tip:: If you're not using the HttpFoundation component, you can use - :class:`Symfony\\Component\\Form\\Extension\\Csrf\\CsrfProvider\\DefaultCsrfProvider` + :class:`Symfony\\Component\\Security\\Csrf\\TokenStorage\\NativeSessionTokenStorage` instead, which relies on PHP's native session handling:: - use Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider; + use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; - $csrfProvider = new DefaultCsrfProvider($csrfSecret); + $csrfStorage = new NativeSessionTokenStorage(); + // ... Twig Templating ~~~~~~~~~~~~~~~ @@ -161,18 +164,12 @@ to easily render your form as HTML form fields (complete with field values, errors, and labels). If you use `Twig`_ as your template engine, the Form component offers a rich integration. -To use the integration, you'll need the ``TwigBridge``, which provides integration -between Twig and several Symfony components. If you're using Composer, you -could install the latest 2.3 version by adding the following ``require`` -line to your ``composer.json`` file: +To use the integration, you'll need the twig bridge, which provides integration +between Twig and several Symfony components: -.. code-block:: json +.. code-block:: terminal - { - "require": { - "symfony/twig-bridge": "2.3.*" - } - } + $ composer require symfony/twig-bridge The TwigBridge integration provides you with several :doc:`Twig Functions ` that help you render the HTML widget, label and error for each field @@ -188,26 +185,29 @@ to bootstrap or access Twig and add the :class:`Symfony\\Bridge\\Twig\\Extension // this file comes with TwigBridge $defaultFormTheme = 'form_div_layout.html.twig'; - $vendorDir = realpath(__DIR__.'/../vendor'); - // the path to TwigBridge so Twig can locate the + $vendorDirectory = realpath(__DIR__.'/../vendor'); + // the path to TwigBridge library so Twig can locate the // form_div_layout.html.twig file - $vendorTwigBridgeDir = - $vendorDir.'/symfony/twig-bridge/Symfony/Bridge/Twig'; + $appVariableReflection = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable'); + $vendorTwigBridgeDirectory = dirname($appVariableReflection->getFileName()); // the path to your other templates - $viewsDir = realpath(__DIR__.'/../views'); + $viewsDirectory = realpath(__DIR__.'/../views'); $twig = new Twig_Environment(new Twig_Loader_Filesystem(array( - $viewsDir, - $vendorTwigBridgeDir.'/Resources/views/Form', + $viewsDirectory, + $vendorTwigBridgeDirectory.'/Resources/views/Form', ))); $formEngine = new TwigRendererEngine(array($defaultFormTheme)); $formEngine->setEnvironment($twig); - // add the FormExtension to Twig + + // ... (see the previous CSRF Protection section for more information) + + // adds the FormExtension to Twig $twig->addExtension( - new FormExtension(new TwigRenderer($formEngine, $csrfProvider)) + new FormExtension(new TwigRenderer($formEngine, $csrfManager)) ); - // create your form factory as normal + // creates a form factory $formFactory = Forms::createFormFactoryBuilder() // ... ->getFormFactory(); @@ -216,10 +216,10 @@ The exact details of your `Twig Configuration`_ will vary, but the goal is always to add the :class:`Symfony\\Bridge\\Twig\\Extension\\FormExtension` to Twig, which gives you access to the Twig functions for rendering forms. To do this, you first need to create a :class:`Symfony\\Bridge\\Twig\\Form\\TwigRendererEngine`, -where you define your :ref:`form themes ` +where you define your :ref:`form themes ` (i.e. resources/files that define form HTML markup). -For general details on rendering forms, see :doc:`/cookbook/form/form_customization`. +For general details on rendering forms, see :doc:`/form/form_customization`. .. note:: @@ -242,18 +242,12 @@ with Symfony's Translation component, or add the 2 Twig filters yourself, via your own Twig extension. To use the built-in integration, be sure that your project has Symfony's -Translation and :doc:`Config ` components -installed. If you're using Composer, you could get the latest 2.3 version -of each of these by adding the following to your ``composer.json`` file: +Translation and :doc:`Config ` components +installed: -.. code-block:: json +.. code-block:: terminal - { - "require": { - "symfony/translation": "2.3.*", - "symfony/config": "2.3.*" - } - } + $ composer require symfony/translation symfony/config Next, add the :class:`Symfony\\Bridge\\Twig\\Extension\\TranslationExtension` to your ``Twig_Environment`` instance:: @@ -263,7 +257,7 @@ to your ``Twig_Environment`` instance:: use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Bridge\Twig\Extension\TranslationExtension; - // create the Translator + // creates the Translator $translator = new Translator('en'); // somehow load some translations into it $translator->addLoader('xlf', new XliffFileLoader()); @@ -273,7 +267,7 @@ to your ``Twig_Environment`` instance:: 'en' ); - // add the TranslationExtension (gives us trans and transChoice filters) + // adds the TranslationExtension (gives us trans and transChoice filters) $twig->addExtension(new TranslationExtension($translator)); $formFactory = Forms::createFormFactoryBuilder() @@ -283,7 +277,7 @@ to your ``Twig_Environment`` instance:: Depending on how your translations are being loaded, you can now add string keys, such as field labels, and their translations to your translation files. -For more details on translations, see :doc:`/book/translation`. +For more details on translations, see :doc:`/translation`. Validation ~~~~~~~~~~ @@ -294,19 +288,14 @@ no problem! Simply take the submitted/bound data of your form (which is an array or object) and pass it through your own validation system. To use the integration with Symfony's Validator component, first make sure -it's installed in your application. If you're using Composer and want to -install the latest 2.3 version, add this to your ``composer.json``: +it's installed in your application: -.. code-block:: json +.. code-block:: terminal - { - "require": { - "symfony/validator": "2.3.*" - } - } + $ composer require symfony/validator If you're not familiar with Symfony's Validator component, read more about -it: :doc:`/book/validation`. The Form component comes with a +it: :doc:`/validation`. The Form component comes with a :class:`Symfony\\Component\\Form\\Extension\\Validator\\ValidatorExtension` class, which automatically applies validation to your data on bind. These errors are then mapped to the correct field and rendered. @@ -317,24 +306,23 @@ Your integration with the Validation component will look something like this:: use Symfony\Component\Form\Extension\Validator\ValidatorExtension; use Symfony\Component\Validator\Validation; - $vendorDir = realpath(__DIR__.'/../vendor'); - $vendorFormDir = $vendorDir.'/symfony/form/Symfony/Component/Form'; - $vendorValidatorDir = - $vendorDir.'/symfony/validator/Symfony/Component/Validator'; + $vendorDirectory = realpath(__DIR__.'/../vendor'); + $vendorFormDirectory = $vendorDir.'/symfony/form'; + $vendorValidatorDirectory = $vendorDirectory.'/symfony/validator'; - // create the validator - details will vary + // creates the validator - details will vary $validator = Validation::createValidator(); // there are built-in translations for the core error messages $translator->addResource( 'xlf', - $vendorFormDir.'/Resources/translations/validators.en.xlf', + $vendorFormDirectory.'/Resources/translations/validators.en.xlf', 'en', 'validators' ); $translator->addResource( 'xlf', - $vendorValidatorDir.'/Resources/translations/validators.en.xlf', + $vendorValidatorDirectory.'/Resources/translations/validators.en.xlf', 'en', 'validators' ); @@ -361,10 +349,12 @@ and then access it whenever you need to build a form. this object in some more "global" way so you can access it from anywhere. Exactly how you gain access to your one form factory is up to you. If you're -using a :term:`Service Container`, then you should add the form factory to -your container and grab it out whenever you need to. If your application -uses global or static variables (not usually a good idea), then you can store -the object on some static class or do something similar. +using a service container (like provided with the +:doc:`DependencyInjection component `), +then you should add the form factory to your container and grab it out whenever +you need to. If your application uses global or static variables (not usually a +good idea), then you can store the object on some static class or do something +similar. Regardless of how you architect your application, just remember that you should only have one form factory and that you'll need to be able to access @@ -380,7 +370,7 @@ Creating a simple Form If you're using the Symfony Framework, then the form factory is available automatically as a service called ``form.factory``. Also, the default base controller class has a :method:`Symfony\\Bundle\\FrameworkBundle\\Controller::createFormBuilder` - method, which is a shortcut to fetch the form factory and call ``createBuilder`` + method, which is a shortcut to fetch the form factory and call ``createBuilder()`` on it. Creating a form is done via a :class:`Symfony\\Component\\Form\\FormBuilder` @@ -419,14 +409,14 @@ is created from the form factory. ->add('dueDate', 'date') ->getForm(); - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( + return $this->render('@AcmeTask/Default/new.html.twig', array( 'form' => $form->createView(), )); } } -As you can see, creating a form is like writing a recipe: you call ``add`` -for each new field you want to create. The first argument to ``add`` is the +As you can see, creating a form is like writing a recipe: you call ``add()`` +for each new field you want to create. The first argument to ``add()`` is the name of your field, and the second is the field "type". The Form component comes with a lot of :doc:`built-in types `. @@ -455,20 +445,33 @@ builder: .. code-block:: php-symfony - $defaults = array( - 'dueDate' => new \DateTime('tomorrow'), - ); + // src/Acme/TaskBundle/Controller/DefaultController.php + namespace Acme\TaskBundle\Controller; - $form = $this->createFormBuilder($defaults) - ->add('task', 'text') - ->add('dueDate', 'date') - ->getForm(); + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + + class DefaultController extends Controller + { + public function newAction(Request $request) + { + $defaults = array( + 'dueDate' => new \DateTime('tomorrow'), + ); + + $form = $this->createFormBuilder($defaults) + ->add('task', 'text') + ->add('dueDate', 'date') + ->getForm(); + + // ... + } + } .. tip:: In this example, the default data is an array. Later, when you use the - :ref:`data_class ` option to bind data directly - to objects, your default data will be an instance of that object. + :ref:`data_class ` option to bind data directly to + objects, your default data will be an instance of that object. .. _component-form-intro-rendering-form: @@ -480,7 +483,7 @@ done by passing a special form "view" object to your template (notice the ``$form->createView()`` in the controller above) and using a set of form helper functions: -.. code-block:: html+jinja +.. code-block:: html+twig {{ form_start(form) }} {{ form_widget(form) }} @@ -488,14 +491,14 @@ helper functions: {{ form_end(form) }} -.. image:: /images/book/form-simple.png +.. image:: /_images/form/simple-form.png :align: center That's it! By printing ``form_widget(form)``, each field in the form is rendered, along with a label and error message (if there is one). As easy as this is, it's not very flexible (yet). Usually, you'll want to render each form field individually so you can control how the form looks. You'll learn how -to do that in the ":ref:`form-rendering-template`" section. +to do that in the ":doc:`/form/rendering`" section. Changing a Form's Method and Action ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -522,16 +525,23 @@ by ``handleRequest()`` to determine whether a form has been submitted): .. code-block:: php-symfony - // ... + // src/Acme/TaskBundle/Controller/DefaultController.php + namespace Acme\TaskBundle\Controller; - public function searchAction() + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\Form\Extension\Core\Type\FormType; + + class DefaultController extends Controller { - $formBuilder = $this->createFormBuilder('form', null, array( - 'action' => '/search', - 'method' => 'GET', - )); + public function searchAction() + { + $formBuilder = $this->createFormBuilder(null, array( + 'action' => '/search', + 'method' => 'GET', + )); - // ... + // ... + } } .. _component-form-intro-handling-submission: @@ -558,7 +568,7 @@ method: $form->handleRequest($request); - if ($form->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { $data = $form->getData(); // ... perform some action, such as saving the data to the database @@ -573,26 +583,32 @@ method: .. code-block:: php-symfony - // ... + // src/Acme/TaskBundle/Controller/DefaultController.php + namespace Acme\TaskBundle\Controller; - public function newAction(Request $request) + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + + class DefaultController extends Controller { - $form = $this->createFormBuilder() - ->add('task', 'text') - ->add('dueDate', 'date') - ->getForm(); + public function newAction(Request $request) + { + $form = $this->createFormBuilder() + ->add('task', 'text') + ->add('dueDate', 'date') + ->getForm(); - $form->handleRequest($request); + $form->handleRequest($request); - if ($form->isValid()) { - $data = $form->getData(); + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); - // ... perform some action, such as saving the data to the database + // ... perform some action, such as saving the data to the database - return $this->redirectToRoute('task_success'); - } + return $this->redirectToRoute('task_success'); + } - // ... + // ... + } } This defines a common form "workflow", which contains 3 different possibilities: @@ -632,27 +648,38 @@ option when building each field: ->add('dueDate', 'date', array( 'constraints' => array( new NotBlank(), - new Type('\DateTime'), + new Type(\DateTime::class), ) )) ->getForm(); .. code-block:: php-symfony + // src/Acme/TaskBundle/Controller/DefaultController.php + namespace Acme\TaskBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Type; - $form = $this->createFormBuilder() - ->add('task', 'text', array( - 'constraints' => new NotBlank(), - )) - ->add('dueDate', 'date', array( - 'constraints' => array( - new NotBlank(), - new Type('\DateTime'), - ) - )) - ->getForm(); + class DefaultController extends Controller + { + public function newAction(Request $request) + { + $form = $this->createFormBuilder() + ->add('task', 'text', array( + 'constraints' => new NotBlank(), + )) + ->add('dueDate', 'date', array( + 'constraints' => array( + new NotBlank(), + new Type(\DateTime::class), + ) + )) + ->getForm(); + // ... + } + } When the form is bound, these validation constraints will be applied automatically and the errors will display next to the fields on error. @@ -674,7 +701,7 @@ method to access the list of errors. It returns a // ... // a FormErrorIterator instance, but only errors attached to this - // form level (e.g. "global errors) + // form level (e.g. global errors) $errors = $form->getErrors(); // a FormErrorIterator instance, but only errors attached to the @@ -696,9 +723,18 @@ method to access the list of errors. It returns a $errorsAsArray = iterator_to_array($form->getErrors()); - This is useful, for example, if you want to use PHP's ``array_`` function + This is useful, for example, if you want to use PHP's ``array_*()`` function on the form errors. +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /form/* + .. _Packagist: https://packagist.org/packages/symfony/form -.. _Twig: http://twig.sensiolabs.org +.. _Twig: http://twig.sensiolabs.org .. _`Twig Configuration`: http://twig.sensiolabs.org/doc/intro.html diff --git a/components/form/index.rst b/components/form/index.rst deleted file mode 100644 index a2de93c6245..00000000000 --- a/components/form/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Form -==== - -.. toctree:: - :maxdepth: 2 - - introduction - type_guesser - form_events diff --git a/components/http_foundation/introduction.rst b/components/http_foundation.rst similarity index 87% rename from components/http_foundation/introduction.rst rename to components/http_foundation.rst index e43f9347b68..8d2603dc36e 100644 --- a/components/http_foundation/introduction.rst +++ b/components/http_foundation.rst @@ -11,7 +11,7 @@ The HttpFoundation Component In PHP, the request is represented by some global variables (``$_GET``, ``$_POST``, ``$_FILES``, ``$_COOKIE``, ``$_SESSION``, ...) and the response is -generated by some functions (``echo``, ``header``, ``setcookie``, ...). +generated by some functions (``echo``, ``header()``, ``setcookie()``, ...). The Symfony HttpFoundation component replaces these default PHP global variables and functions by an object-oriented layer. @@ -19,10 +19,11 @@ variables and functions by an object-oriented layer. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/http-foundation`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/HttpFoundation). + $ composer require symfony/http-foundation + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -69,7 +70,7 @@ can be accessed via several public properties: * ``server``: equivalent of ``$_SERVER``; -* ``headers``: mostly equivalent to a sub-set of ``$_SERVER`` +* ``headers``: mostly equivalent to a subset of ``$_SERVER`` (``$request->headers->get('User-Agent')``). Each property is a :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` @@ -90,7 +91,7 @@ instance (or a sub-class of), which is a data holder class: * ``headers``: :class:`Symfony\\Component\\HttpFoundation\\HeaderBag`. All :class:`Symfony\\Component\\HttpFoundation\\ParameterBag` instances have -methods to retrieve and update its data: +methods to retrieve and update their data: :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::all` Returns the parameters. @@ -125,6 +126,12 @@ has some methods to filter the input values: :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getAlnum` Returns the alphabetic characters and digits of the parameter value; +:method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getBoolean` + Returns the parameter value converted to boolean; + + .. versionadded:: 2.6 + The ``getBoolean()`` method was introduced in Symfony 2.6. + :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::getDigits` Returns the digits of the parameter value; @@ -134,7 +141,7 @@ has some methods to filter the input values: :method:`Symfony\\Component\\HttpFoundation\\ParameterBag::filter` Filters the parameter by using the PHP :phpfunction:`filter_var` function. -All getters takes up to three arguments: the first one is the parameter name +All getters take up to three arguments: the first one is the parameter name and the second one is the default value to return if the parameter does not exist:: @@ -157,16 +164,16 @@ sometimes, you might want to get the value for the "original" parameter name: :method:`Symfony\\Component\\HttpFoundation\\Request::get` via the third argument:: - // the query string is '?foo[bar]=bar' + // the query string is '?foo[bar]=bar' - $request->query->get('foo'); - // returns array('bar' => 'bar') + $request->query->get('foo'); + // returns array('bar' => 'bar') - $request->query->get('foo[bar]'); - // returns null + $request->query->get('foo[bar]'); + // returns null - $request->query->get('foo[bar]', null, true); - // returns 'bar' + $request->query->get('foo[bar]', null, true); + // returns 'bar' .. _component-foundation-attributes: @@ -174,9 +181,7 @@ Thanks to the public ``attributes`` property, you can store additional data in the request, which is also an instance of :class:`Symfony\\Component\\HttpFoundation\\ParameterBag`. This is mostly used to attach information that belongs to the Request and that needs to be -accessed from many different points in your application. For information -on how this is used in the Symfony Framework, see -:ref:`the Symfony book `. +accessed from many different points in your application. Finally, the raw data sent with the request body can be accessed using :method:`Symfony\\Component\\HttpFoundation\\Request::getContent`:: @@ -237,8 +242,8 @@ the method tells you if the request contains a session which was started in one of the previous requests. -Accessing `Accept-*` Headers Data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Accessing ``Accept-*`` Headers Data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can easily access basic data extracted from ``Accept-*`` headers by using the following methods: @@ -255,24 +260,21 @@ by using the following methods: :method:`Symfony\\Component\\HttpFoundation\\Request::getEncodings` Returns the list of accepted encodings ordered by descending quality. - .. versionadded:: 2.4 - The ``getEncodings()`` method was introduced in Symfony 2.4. - If you need to get full access to parsed data from ``Accept``, ``Accept-Language``, ``Accept-Charset`` or ``Accept-Encoding``, you can use :class:`Symfony\\Component\\HttpFoundation\\AcceptHeader` utility class:: use Symfony\Component\HttpFoundation\AcceptHeader; - $accept = AcceptHeader::fromString($request->headers->get('Accept')); - if ($accept->has('text/html')) { - $item = $accept->get('text/html'); + $acceptHeader = AcceptHeader::fromString($request->headers->get('Accept')); + if ($acceptHeader->has('text/html')) { + $item = $acceptHeader->get('text/html'); $charset = $item->getAttribute('charset', 'utf-8'); $quality = $item->getQuality(); } // Accept header items are sorted by descending quality - $accepts = AcceptHeader::fromString($request->headers->get('Accept')) + $acceptHeaders = AcceptHeader::fromString($request->headers->get('Accept')) ->all(); Accessing other Data @@ -291,6 +293,7 @@ represents an HTTP message. But when moving from a legacy system, adding methods or changing some default behavior might help. In that case, register a PHP callable that is able to create an instance of your ``Request`` class:: + use AppBundle\Http\SpecialRequest; use Symfony\Component\HttpFoundation\Request; Request::setFactory(function ( @@ -302,7 +305,7 @@ PHP callable that is able to create an instance of your ``Request`` class:: array $server = array(), $content = null ) { - return SpecialRequest::create( + return new SpecialRequest( $query, $request, $attributes, @@ -354,9 +357,9 @@ UTF-8. Sending the Response ~~~~~~~~~~~~~~~~~~~~ -Before sending the Response, you can ensure that it is compliant with the HTTP -specification by calling the -:method:`Symfony\\Component\\HttpFoundation\\Response::prepare` method:: +Before sending the Response, you can optionally call the +:method:`Symfony\\Component\\HttpFoundation\\Response::prepare` method to fix any +incompatibility with the HTTP specification (e.g. a wrong ``Content-Type`` header):: $response->prepare($request); @@ -426,6 +429,8 @@ method:: If the Response is not modified, it sets the status code to 304 and removes the actual response content. +.. _redirect-response: + Redirecting the User ~~~~~~~~~~~~~~~~~~~~ @@ -464,8 +469,12 @@ represented by a PHP callable instead of a string:: you must call ``ob_flush()`` before ``flush()``. Additionally, PHP isn't the only layer that can buffer output. Your web - server might also buffer based on its configuration. Even more, if you - use fastcgi, buffering can't be disabled at all. + server might also buffer based on its configuration. Some servers, such as + Nginx, let you disable buffering at config level or adding a special HTTP + header in the response:: + + // disables FastCGI buffering in Nginx only for this response + $response->headers->set('X-Accel-Buffering', 'no') .. _component-http-foundation-serving-files: @@ -478,14 +487,18 @@ non-ASCII filenames is more involving. The :method:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag::makeDisposition` abstracts the hard work behind a simple API:: + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; - $d = $response->headers->makeDisposition( + $fileContent = ...; // the generated file content + $response = new Response($fileContent); + + $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'foo.pdf' ); - $response->headers->set('Content-Disposition', $d); + $response->headers->set('Content-Disposition', $disposition); Alternatively, if you are serving a static file, you can use a :class:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse`:: @@ -504,8 +517,10 @@ if it should:: BinaryFileResponse::trustXSendfileTypeHeader(); -You can still set the ``Content-Type`` of the sent file, or change its ``Content-Disposition``:: +With the ``BinaryFileResponse``, you can still set the ``Content-Type`` of the sent file, +or change its ``Content-Disposition``:: + // ... $response->headers->set('Content-Type', 'text/plain'); $response->setContentDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, @@ -515,10 +530,17 @@ You can still set the ``Content-Type`` of the sent file, or change its ``Content .. versionadded:: 2.6 The ``deleteFileAfterSend()`` method was introduced in Symfony 2.6. -It is possible to delete the file after the request is sent with the +It is possible to delete the file after the request is sent with the :method:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse::deleteFileAfterSend` method. Please note that this will not work when the ``X-Sendfile`` header is set. +.. note:: + + If you *just* created the file during this same request, the file *may* be sent + without any content. This may be due to cached file stats that return zero for + the size of the file. To fix this issue, call ``clearstatcache(true, $file)`` + with the path to the binary file. + .. _component-http-foundation-json-response: Creating a JSON Response @@ -543,7 +565,7 @@ class, which can make this even easier:: $response = new JsonResponse(); $response->setData(array( - 'data' => 123 + 'data' => 123, )); This encodes your array of data to JSON and sets the ``Content-Type`` header @@ -580,6 +602,19 @@ Session The session information is in its own document: :doc:`/components/http_foundation/sessions`. +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/http_foundation/* + /controller + /controller/* + /session/* + /http_cache/* + .. _Packagist: https://packagist.org/packages/symfony/http-foundation .. _Nginx: http://wiki.nginx.org/XSendfile .. _Apache: https://tn123.org/mod_xsendfile/ diff --git a/components/http_foundation/index.rst b/components/http_foundation/index.rst deleted file mode 100644 index 39d8f04863d..00000000000 --- a/components/http_foundation/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -HttpFoundation -============== - -.. toctree:: - :maxdepth: 2 - - introduction - sessions - session_configuration - session_testing - session_php_bridge - trusting_proxies diff --git a/components/http_foundation/session_configuration.rst b/components/http_foundation/session_configuration.rst index 33878afa906..ba7c95766ef 100644 --- a/components/http_foundation/session_configuration.rst +++ b/components/http_foundation/session_configuration.rst @@ -45,8 +45,8 @@ Example usage:: use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; - $storage = new NativeSessionStorage(array(), new NativeFileSessionHandler()); - $session = new Session($storage); + $sessionStorage = new NativeSessionStorage(array(), new NativeFileSessionHandler()); + $session = new Session($sessionStorage); .. note:: @@ -84,8 +84,8 @@ Example usage:: use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; $pdo = new \PDO(...); - $storage = new NativeSessionStorage(array(), new PdoSessionHandler($pdo)); - $session = new Session($storage); + $sessionStorage = new NativeSessionStorage(array(), new PdoSessionHandler($pdo)); + $session = new Session($sessionStorage); Configuring PHP Sessions ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -139,6 +139,20 @@ the ``php.ini`` directive ``session.gc_maxlifetime``. The meaning in this contex that any stored session that was saved more than ``gc_maxlifetime`` ago should be deleted. This allows one to expire records based on idle time. +However, some operating systems do their own session handling and set the +``session.gc_probability`` variable to ``0`` to stop PHP doing garbage +collection. That's why Symfony now overwrites this value to ``1``. + +If you wish to use the original value set in your ``php.ini``, add the following +configuration: + +.. code-block:: yaml + + # config.yml + framework: + session: + gc_probability: null + You can configure these settings by passing ``gc_probability``, ``gc_divisor`` and ``gc_maxlifetime`` in an array to the constructor of :class:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage` @@ -217,7 +231,7 @@ response headers. use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; $options['cache_limiter'] = session_cache_limiter(); - $storage = new NativeSessionStorage($options); + $sessionStorage = new NativeSessionStorage($options); Session Metadata ~~~~~~~~~~~~~~~~ @@ -300,6 +314,6 @@ without knowledge of the specific save handler. Before PHP 5.4, you can only proxy user-land save handlers but not native PHP save handlers. -.. _`php.net/session.customhandler`: http://php.net/session.customhandler -.. _`php.net/session.configuration`: http://php.net/session.configuration -.. _`php.net/memcached.setoption`: http://php.net/memcached.setoption +.. _`php.net/session.customhandler`: https://php.net/session.customhandler +.. _`php.net/session.configuration`: https://php.net/session.configuration +.. _`php.net/memcached.setoption`: https://php.net/memcached.setoption diff --git a/components/http_foundation/session_testing.rst b/components/http_foundation/session_testing.rst index 1bc80b03fbb..f6c290fa132 100644 --- a/components/http_foundation/session_testing.rst +++ b/components/http_foundation/session_testing.rst @@ -21,16 +21,16 @@ The mock storage drivers do not read or write the system globals ``session_id()`` or ``session_name()``. Methods are provided to simulate this if required: -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::getId`: Gets the +* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::getId`: Gets the session ID. -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::setId`: Sets the +* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::setId`: Sets the session ID. -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::getName`: Gets the +* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::getName`: Gets the session name. -* :method:`Symfony\\Component\\HttpFoundation\\Session\\SessionStorageInterface::setName`: Sets the +* :method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface::setName`: Sets the session name. Unit Testing diff --git a/components/http_foundation/sessions.rst b/components/http_foundation/sessions.rst index 0034963f331..60b639c8dd9 100644 --- a/components/http_foundation/sessions.rst +++ b/components/http_foundation/sessions.rst @@ -226,9 +226,6 @@ has a simple API :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::has` Returns true if the attribute exists. -:method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::keys` - Returns an array of stored attribute keys. - :method:`Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface::replace` Sets multiple attributes at once: takes a keyed array and sets each key => value pair. diff --git a/components/http_foundation/trusting_proxies.rst b/components/http_foundation/trusting_proxies.rst index fbe9b30cdee..8c9687aeb2a 100644 --- a/components/http_foundation/trusting_proxies.rst +++ b/components/http_foundation/trusting_proxies.rst @@ -7,34 +7,41 @@ Trusting Proxies .. tip:: If you're using the Symfony Framework, start by reading - :doc:`/cookbook/request/load_balancer_reverse_proxy`. + :doc:`/deployment/proxies`. If you find yourself behind some sort of proxy - like a load balancer - then certain header information may be sent to you using special ``X-Forwarded-*`` -headers. For example, the ``Host`` HTTP header is usually used to return -the requested host. But when you're behind a proxy, the true host may be -stored in a ``X-Forwarded-Host`` header. +headers or the ``Forwarded`` header. For example, the ``Host`` HTTP header is +usually used to return the requested host. But when you're behind a proxy, +the actual host may be stored in an ``X-Forwarded-Host`` header. Since HTTP headers can be spoofed, Symfony does *not* trust these proxy headers by default. If you are behind a proxy, you should manually whitelist -your proxy. +your proxy as follows:: + + use Symfony\Component\HttpFoundation\Request; + + // put this code as early as possible in your application (e.g. in your + // front controller) to only trust proxy headers coming from these IP addresses + Request::setTrustedProxies(array('192.0.0.1', '10.0.0.0/8')); .. versionadded:: 2.3 CIDR notation support was introduced in Symfony 2.3, so you can whitelist whole subnets (e.g. ``10.0.0.0/8``, ``fc00::/7``). -.. code-block:: php +You should also make sure that your proxy filters unauthorized use of these +headers, e.g. if a proxy natively uses the ``X-Forwarded-For`` header, it +should not allow clients to send ``Forwarded`` headers to Symfony. - use Symfony\Component\HttpFoundation\Request; - - // only trust proxy headers coming from this IP addresses - Request::setTrustedProxies(array('192.0.0.1', '10.0.0.0/8')); +If your proxy does not filter headers appropriately, you need to configure +Symfony not to trust the headers your proxy does not filter (see below). Configuring Header Names ------------------------ By default, the following proxy headers are trusted: +* ``Forwarded`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getClientIp`; * ``X-Forwarded-For`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getClientIp`; * ``X-Forwarded-Host`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getHost`; * ``X-Forwarded-Port`` Used in :method:`Symfony\\Component\\HttpFoundation\\Request::getPort`; @@ -43,6 +50,7 @@ By default, the following proxy headers are trusted: If your reverse proxy uses a different header name for any of these, you can configure that header name via :method:`Symfony\\Component\\HttpFoundation\\Request::setTrustedHeaderName`:: + Request::setTrustedHeaderName(Request::HEADER_FORWARDED, 'X-Forwarded'); Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, 'X-Proxy-For'); Request::setTrustedHeaderName(Request::HEADER_CLIENT_HOST, 'X-Proxy-Host'); Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, 'X-Proxy-Port'); @@ -51,9 +59,9 @@ can configure that header name via :method:`Symfony\\Component\\HttpFoundation\\ Not Trusting certain Headers ---------------------------- -By default, if you whitelist your proxy's IP address, then all four headers +By default, if you whitelist your proxy's IP address, then all five headers listed above are trusted. If you need to trust some of these headers but not others, you can do that as well:: - // disables trusting the ``X-Forwarded-Proto`` header, the default header is used - Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, ''); + // disables trusting the ``Forwarded`` header + Request::setTrustedHeaderName(Request::HEADER_FORWARDED, null); diff --git a/components/http_kernel/introduction.rst b/components/http_kernel.rst similarity index 88% rename from components/http_kernel/introduction.rst rename to components/http_kernel.rst index 65dd1c29177..2b24d38c5c0 100644 --- a/components/http_kernel/introduction.rst +++ b/components/http_kernel.rst @@ -14,10 +14,11 @@ The HttpKernel Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/http-kernel`` on Packagist_); -* Use the official Git repository (https://github.com/symfony/HttpKernel). + $ composer require symfony/http-kernel + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -27,9 +28,14 @@ The Workflow of a Request Every HTTP web interaction begins with a request and ends with a response. Your job as a developer is to create PHP code that reads the request information (e.g. the URL) and creates and returns a response (e.g. an HTML page or JSON string). +This is a simplified overview of the request workflow in Symfony applications: -.. image:: /images/components/http_kernel/request-response-flow.png - :align: center +#. The **user** asks for a **resource** in a **browser**; +#. The **browser** sends a **request** to the **server**; +#. **Symfony** gives the **application** a **Request** object; +#. The **application** generates a **Response** object using the data of the **Request** object; +#. The **server** sends back the **response** to the **browser**; +#. The **browser** displays the **resource** to the **user**. Typically, some sort of framework or system is built to handle all the repetitive tasks (e.g. routing, security, etc) so that a developer can easily build @@ -62,8 +68,9 @@ the concrete implementation of :method:`HttpKernelInterface::handle() The exact details of this workflow are the key to understanding how the kernel (and the Symfony Framework or any other library that uses the kernel) works. @@ -82,7 +89,7 @@ Framework - works. Initially, using the :class:`Symfony\\Component\\HttpKernel\\HttpKernel` is really simple and involves creating an -:doc:`event dispatcher ` and a +:doc:`event dispatcher ` and a :ref:`controller resolver ` (explained below). To complete your working kernel, you'll add more event listeners to the events discussed below:: @@ -110,7 +117,7 @@ to the events discussed below:: // send the headers and echo the content $response->send(); - // triggers the kernel.terminate event + // trigger the kernel.terminate event $kernel->terminate($request, $response); See ":ref:`http-kernel-working-example`" for a more concrete implementation. @@ -118,11 +125,11 @@ See ":ref:`http-kernel-working-example`" for a more concrete implementation. For general information on adding listeners to the events below, see :ref:`http-kernel-creating-listener`. -.. tip:: +.. seealso:: - Fabien Potencier also wrote a wonderful series on using the HttpKernel - component and other Symfony components to create your own framework. See - `Create your own framework... on top of the Symfony2 Components`_. + There is a wonderful tutorial series on using the HttpKernel component and + other Symfony components to create your own framework. See + :doc:`/create_framework/introduction`. .. _component-http-kernel-kernel-request: @@ -138,9 +145,6 @@ layer that denies access). The first event that is dispatched inside :method:`HttpKernel::handle ` is ``kernel.request``, which may have a variety of different listeners. -.. image:: /images/components/http_kernel/02-kernel-request.png - :align: center - Listeners of this event can be quite varied. Some listeners - such as a security listener - might have enough information to create a ``Response`` object immediately. For example, if a security listener determined that a user doesn't have access, @@ -150,9 +154,6 @@ to the login page or a 403 Access Denied response. If a ``Response`` is returned at this stage, the process skips directly to the :ref:`kernel.response ` event. -.. image:: /images/components/http_kernel/03-kernel-request-response.png - :align: center - Other listeners simply initialize things or add more information to the request. For example, a listener might determine and set the locale on the ``Request`` object. @@ -182,7 +183,7 @@ attributes). This class executes the routing layer, which returns an *array* of information about the matched request, including the ``_controller`` and any placeholders that are in the route's pattern (e.g. ``{slug}``). See - :doc:`Routing component `. + :doc:`Routing component `. This array of information is stored in the :class:`Symfony\\Component\\HttpFoundation\\Request` object's ``attributes`` array. Adding the routing information here doesn't @@ -205,11 +206,8 @@ to your application. This is the job of the "controller resolver" - a class that implements :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface` and is one of the constructor arguments to ``HttpKernel``. -.. image:: /images/components/http_kernel/04-resolve-controller.png - :align: center - Your job is to create a class that implements the interface and fill in its -two methods: ``getController`` and ``getArguments``. In fact, one default +two methods: ``getController()`` and ``getArguments()``. In fact, one default implementation already exists, which you can use directly or learn from: :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver`. This implementation is explained more in the sidebar below:: @@ -225,7 +223,7 @@ This implementation is explained more in the sidebar below:: public function getArguments(Request $request, $controller); } -Internally, the ``HttpKernel::handle`` method first calls +Internally, the ``HttpKernel::handle()`` method first calls :method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getController` on the controller resolver. This method is passed the ``Request`` and is responsible for somehow determining and returning a PHP callable (the controller) based @@ -260,13 +258,10 @@ will be called after another event - ``kernel.controller`` - is dispatched. constructor arguments. c) If the controller implements :class:`Symfony\\Component\\DependencyInjection\\ContainerAwareInterface`, - ``setContainer`` is called on the controller object and the container + ``setContainer()`` is called on the controller object and the container is passed to it. This step is also specific to the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerResolver` sub-class used by the Symfony Framework. - There are also a few other variations on the above process (e.g. if - you're registering your controllers as services). - .. _component-http-kernel-kernel-controller: 3) The ``kernel.controller`` Event @@ -277,15 +272,12 @@ the controller is executed. :ref:`Kernel Events Information Table ` -After the controller callable has been determined, ``HttpKernel::handle`` +After the controller callable has been determined, ``HttpKernel::handle()`` dispatches the ``kernel.controller`` event. Listeners to this event might initialize some part of the system that needs to be initialized after certain things have been determined (e.g. the controller, routing information) but before the controller is executed. For some examples, see the Symfony section below. -.. image:: /images/components/http_kernel/06-kernel-controller.png - :align: center - Listeners to this event can also change the controller callable completely by calling :method:`FilterControllerEvent::setController ` on the event object that's passed to listeners on this event. @@ -309,17 +301,14 @@ on the event object that's passed to listeners on this event. 4) Getting the Controller Arguments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Next, ``HttpKernel::handle`` calls +Next, ``HttpKernel::handle()`` calls :method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getArguments`. -Remember that the controller returned in ``getController`` is a callable. -The purpose of ``getArguments`` is to return the array of arguments that +Remember that the controller returned in ``getController()`` is a callable. +The purpose of ``getArguments()`` is to return the array of arguments that should be passed to that controller. Exactly how this is done is completely up to your design, though the built-in :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver` is a good example. -.. image:: /images/components/http_kernel/07-controller-arguments.png - :align: center - At this point the kernel has a PHP callable (the controller) and an array of arguments that should be passed when executing that callable. @@ -346,10 +335,7 @@ of arguments that should be passed when executing that callable. 5) Calling the Controller ~~~~~~~~~~~~~~~~~~~~~~~~~ -The next step is simple! ``HttpKernel::handle`` executes the controller. - -.. image:: /images/components/http_kernel/08-call-controller.png - :align: center +The next step is simple! ``HttpKernel::handle()`` executes the controller. The job of the controller is to build the response for the given resource. This could be an HTML page, a JSON string or anything else. Unlike every @@ -360,9 +346,6 @@ Usually, the controller will return a ``Response`` object. If this is true, then the work of the kernel is just about done! In this case, the next step is the :ref:`kernel.response ` event. -.. image:: /images/components/http_kernel/09-controller-returns-response.png - :align: center - But if the controller returns anything besides a ``Response``, then the kernel has a little bit more work to do - :ref:`kernel.view ` (since the end goal is *always* to generate a ``Response`` object). @@ -387,9 +370,6 @@ another event - ``kernel.view``. The job of a listener to this event is to use the return value of the controller (e.g. an array of data or an object) to create a ``Response``. -.. image:: /images/components/http_kernel/10-kernel-view.png - :align: center - This can be useful if you want to use a "view" layer: instead of returning a ``Response`` from the controller, you return data that represents the page. A listener to this event could then use this data to create a ``Response`` that @@ -467,11 +447,11 @@ been streamed to the user :ref:`Kernel Events Information Table ` The final event of the HttpKernel process is ``kernel.terminate`` and is unique -because it occurs *after* the ``HttpKernel::handle`` method, and after the +because it occurs *after* the ``HttpKernel::handle()`` method, and after the response is sent to the user. Recall from above, then the code that uses the kernel, ends like this:: - // send the headers and echo the content + // sends the headers and echoes the content $response->send(); // triggers the kernel.terminate event @@ -512,14 +492,15 @@ Handling Exceptions: the ``kernel.exception`` Event :ref:`Kernel Events Information Table ` -If an exception is thrown at any point inside ``HttpKernel::handle``, another -event - ``kernel.exception`` is thrown. Internally, the body of the ``handle`` +If an exception is thrown at any point inside ``HttpKernel::handle()``, another +event - ``kernel.exception`` is thrown. Internally, the body of the ``handle()`` function is wrapped in a try-catch block. When any exception is thrown, the ``kernel.exception`` event is dispatched so that your system can somehow respond to the exception. -.. image:: /images/components/http_kernel/11-kernel-exception.png - :align: center +.. raw:: html + + Each listener to this event is passed a :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent` object, which you can use to access the original exception via the @@ -551,13 +532,13 @@ below for more details). The listener has several goals: 1) The thrown exception is converted into a - :class:`Symfony\\Component\\HttpKernel\\Exception\\FlattenException` + :class:`Symfony\\Component\\Debug\\Exception\\FlattenException` object, which contains all the information about the request, but which can be printed and serialized. 2) If the original exception implements :class:`Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface`, - then ``getStatusCode`` and ``getHeaders`` are called on the exception + then ``getStatusCode()`` and ``getHeaders()`` are called on the exception and used to populate the headers and status code of the ``FlattenException`` object. The idea is that these are used in the next step when creating the final response. @@ -580,9 +561,9 @@ Creating an Event Listener -------------------------- As you've seen, you can create and attach event listeners to any of the events -dispatched during the ``HttpKernel::handle`` cycle. Typically a listener is a PHP +dispatched during the ``HttpKernel::handle()`` cycle. Typically a listener is a PHP class with a method that's executed, but it can be anything. For more information -on creating and attaching event listeners, see :doc:`/components/event_dispatcher/introduction`. +on creating and attaching event listeners, see :doc:`/components/event_dispatcher`. The name of each of the "kernel" events is defined as a constant on the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` class. Additionally, each @@ -595,7 +576,7 @@ each event has their own event object: ===================== ================================ =================================================================================== Name ``KernelEvents`` Constant Argument passed to the listener ===================== ================================ =================================================================================== -kernel.request ``KernelEvents::REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent` +kernel.request ``KernelEvents::REQUEST`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent` kernel.controller ``KernelEvents::CONTROLLER`` :class:`Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent` kernel.view ``KernelEvents::VIEW`` :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent` kernel.response ``KernelEvents::RESPONSE`` :class:`Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent` @@ -656,17 +637,18 @@ a built-in ControllerResolver that can be used to create a working example:: Sub Requests ------------ -In addition to the "main" request that's sent into ``HttpKernel::handle``, +In addition to the "main" request that's sent into ``HttpKernel::handle()``, you can also send so-called "sub request". A sub request looks and acts like any other request, but typically serves to render just one small portion of a page instead of a full page. You'll most commonly make sub-requests from your controller (or perhaps from inside a template, that's being rendered by your controller). -.. image:: /images/components/http_kernel/sub-request.png - :align: center +.. raw:: html -To execute a sub request, use ``HttpKernel::handle``, but change the second + + +To execute a sub request, use ``HttpKernel::handle()``, but change the second argument as follows:: use Symfony\Component\HttpFoundation\Request; @@ -704,12 +686,47 @@ look like this:: // ... } +.. _http-kernel-resource-locator: + +Locating Resources +------------------ + +The HttpKernel component is responsible of the bundle mechanism used in Symfony +applications. The key feature of the bundles is that they allow to override any +resource used by the application (config files, templates, controllers, +translation files, etc.) + +This overriding mechanism works because resources are referenced not by their +physical path but by their logical path. For example, the ``services.xml`` file +stored in the ``Resources/config/`` directory of a bundle called AppBundle is +referenced as ``@AppBundle/Resources/config/services.xml``. This logical path +will work when the application overrides that file and even if you change the +directory of AppBundle. + +The HttpKernel component provides a method called :method:`Symfony\\Component\\HttpKernel\\Kernel::locateResource` +which can be used to transform logical paths into physical paths:: + + use Symfony\Component\HttpKernel\HttpKernel; + + // ... + $kernel = new HttpKernel($dispatcher, $resolver); + $path = $kernel->locateResource('@AppBundle/Resources/config/services.xml'); + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /reference/events + .. _Packagist: https://packagist.org/packages/symfony/http-kernel -.. _reflection: http://php.net/manual/en/book.reflection.php +.. _reflection: https://php.net/manual/en/book.reflection.php .. _FOSRestBundle: https://github.com/friendsofsymfony/FOSRestBundle .. _`Create your own framework... on top of the Symfony2 Components`: http://fabien.potencier.org/article/50/create-your-own-framework-on-top-of-the-symfony2-components-part-1 -.. _`PHP FPM`: http://php.net/manual/en/install.fpm.php +.. _`PHP FPM`: https://php.net/manual/en/install.fpm.php .. _`SensioFrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html .. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html .. _`@Template`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/view.html -.. _`EmailSenderListener`: https://github.com/symfony/SwiftmailerBundle/blob/master/EventListener/EmailSenderListener.php +.. _`EmailSenderListener`: https://github.com/symfony/swiftmailer-bundle/blob/master/EventListener/EmailSenderListener.php diff --git a/components/http_kernel/index.rst b/components/http_kernel/index.rst deleted file mode 100644 index 485c69b1019..00000000000 --- a/components/http_kernel/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -HttpKernel -========== - -.. toctree:: - :maxdepth: 2 - - introduction diff --git a/components/index.rst b/components/index.rst index 41f72d049a4..14f7e7b6f51 100644 --- a/components/index.rst +++ b/components/index.rst @@ -2,35 +2,8 @@ The Components ============== .. toctree:: - :hidden: + :maxdepth: 1 + :glob: using_components - asset/index - class_loader/index - config/index - console/index - css_selector - debug/index - dependency_injection/index - dom_crawler - event_dispatcher/index - expression_language/index - filesystem/index - finder - form/index - http_foundation/index - http_kernel/index - intl - options_resolver - process - property_access/index - routing/index - security/index - serializer - stopwatch - templating/index - translation/index - var_dumper/index - yaml/index - -.. include:: /components/map.rst.inc + * diff --git a/components/intl.rst b/components/intl.rst index af85a06aabb..1322ff49302 100644 --- a/components/intl.rst +++ b/components/intl.rst @@ -20,10 +20,13 @@ The Intl Component Installation ------------ -You can install the component in two different ways: +.. code-block:: terminal -* :doc:`Install it via Composer` (``symfony/intl`` on `Packagist`_); -* Using the official Git repository (https://github.com/symfony/Intl). + $ composer require symfony/intl + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc If you install the component via Composer, the following classes and functions of the intl extension will be automatically provided if the intl extension is @@ -50,7 +53,7 @@ replace the intl classes: Composer automatically exposes these classes in the global namespace. If you don't use Composer but the -:doc:`Symfony ClassLoader component `, +:doc:`Symfony ClassLoader component `, you need to expose them manually by adding the following lines to your autoload code:: @@ -192,10 +195,10 @@ returned:: $data = $reader->read('/path/to/bundle', 'en'); - // Produces an error if the key "Data" does not exist + // produces an error if the key "Data" does not exist var_dump($data['Data']['entry1']); - // Returns null if the key "Data" does not exist + // returns null if the key "Data" does not exist var_dump($reader->readEntry('/path/to/bundle', 'en', array('Data', 'entry1'))); Additionally, the @@ -337,8 +340,20 @@ to the current default locale:: That's all you need to know for now. Have fun coding! +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /reference/forms/types/country + /reference/forms/types/currency + /reference/forms/types/language + /reference/forms/types/locale + .. _Packagist: https://packagist.org/packages/symfony/intl .. _Icu component: https://packagist.org/packages/symfony/icu -.. _intl extension: http://www.php.net/manual/en/book.intl.php -.. _install the intl extension: http://www.php.net/manual/en/intl.setup.php +.. _intl extension: https://php.net/manual/en/book.intl.php +.. _install the intl extension: https://php.net/manual/en/intl.setup.php .. _ICU library: http://site.icu-project.org/ diff --git a/components/map.rst.inc b/components/map.rst.inc deleted file mode 100644 index dfead75f62c..00000000000 --- a/components/map.rst.inc +++ /dev/null @@ -1,161 +0,0 @@ -* :doc:`/components/using_components` - -* :doc:`/components/asset/index` - - * :doc:`/components/asset/introduction` - -* :doc:`/components/class_loader/index` - - * :doc:`/components/class_loader/introduction` - * :doc:`/components/class_loader/class_loader` - * :doc:`/components/class_loader/psr4_class_loader` - * :doc:`/components/class_loader/map_class_loader` - * :doc:`/components/class_loader/cache_class_loader` - * :doc:`/components/class_loader/class_map_generator` - -* :doc:`/components/config/index` - - * :doc:`/components/config/introduction` - * :doc:`/components/config/resources` - * :doc:`/components/config/caching` - * :doc:`/components/config/definition` - -* :doc:`/components/console/index` - - * :doc:`/components/console/introduction` - * :doc:`/components/console/usage` - * :doc:`/components/console/single_command_tool` - * :doc:`/components/console/changing_default_command` - * :doc:`/components/console/console_arguments` - * :doc:`/components/console/events` - * :doc:`/components/console/logger` - * :doc:`/components/console/helpers/index` - -* **CssSelector** - - * :doc:`/components/css_selector` - -* :doc:`/components/debug/index` - - * :doc:`/components/debug/introduction` - * :doc:`/components/debug/class_loader` - -* :doc:`/components/dependency_injection/index` - - * :doc:`/components/dependency_injection/introduction` - * :doc:`/components/dependency_injection/types` - * :doc:`/components/dependency_injection/parameters` - * :doc:`/components/dependency_injection/definitions` - * :doc:`/components/dependency_injection/synthetic_services` - * :doc:`/components/dependency_injection/compilation` - * :doc:`/components/dependency_injection/tags` - * :doc:`/components/dependency_injection/factories` - * :doc:`/components/dependency_injection/configurators` - * :doc:`/components/dependency_injection/parentservices` - * :doc:`/components/dependency_injection/advanced` - * :doc:`/components/dependency_injection/lazy_services` - * :doc:`/components/dependency_injection/workflow` - -* **DomCrawler** - - * :doc:`/components/dom_crawler` - -* :doc:`/components/event_dispatcher/index` - - * :doc:`/components/event_dispatcher/introduction` - * :doc:`/components/event_dispatcher/container_aware_dispatcher` - * :doc:`/components/event_dispatcher/generic_event` - * :doc:`/components/event_dispatcher/immutable_dispatcher` - * :doc:`/components/event_dispatcher/traceable_dispatcher` - -* :doc:`/components/expression_language/index` - - * :doc:`/components/expression_language/introduction` - * :doc:`/components/expression_language/syntax` - * :doc:`/components/expression_language/extending` - * :doc:`/components/expression_language/caching` - -* :doc:`/components/filesystem/index` - - * :doc:`/components/filesystem/introduction` - * :doc:`/components/filesystem/lock_handler` - -* **Finder** - - * :doc:`/components/finder` - -* :doc:`/components/form/index` - - * :doc:`/components/form/introduction` - * :doc:`/components/form/form_events` - * :doc:`/components/form/type_guesser` - -* :doc:`/components/http_foundation/index` - - * :doc:`/components/http_foundation/introduction` - * :doc:`/components/http_foundation/sessions` - * :doc:`/components/http_foundation/session_configuration` - * :doc:`/components/http_foundation/session_testing` - * :doc:`/components/http_foundation/session_php_bridge` - * :doc:`/components/http_foundation/trusting_proxies` - -* :doc:`/components/http_kernel/index` - - * :doc:`/components/http_kernel/introduction` - -* **Intl** - - * :doc:`/components/intl` - -* **OptionsResolver** - - * :doc:`/components/options_resolver` - -* **Process** - - * :doc:`/components/process` - -* :doc:`/components/property_access/index` - - * :doc:`/components/property_access/introduction` - -* :doc:`/components/routing/index` - - * :doc:`/components/routing/introduction` - * :doc:`/components/routing/hostname_pattern` - -* :doc:`/components/security/index` - - * :doc:`/components/security/introduction` - * :doc:`/components/security/firewall` - * :doc:`/components/security/authentication` - * :doc:`/components/security/authorization` - * :doc:`/components/security/secure_tools` - -* **Serializer** - - * :doc:`/components/serializer` - -* **Stopwatch** - - * :doc:`/components/stopwatch` - -* :doc:`/components/templating/index` - - * :doc:`/components/templating/introduction` - -* :doc:`/components/translation/index` - - * :doc:`/components/translation/introduction` - * :doc:`/components/translation/usage` - * :doc:`/components/translation/custom_formats` - -* :doc:`/components/var_dumper/index` - - * :doc:`/components/var_dumper/introduction` - * :doc:`/components/var_dumper/advanced` - -* :doc:`/components/yaml/index` - - * :doc:`/components/yaml/introduction` - * :doc:`/components/yaml/yaml_format` diff --git a/components/options_resolver.rst b/components/options_resolver.rst index b05ba845d6d..5ea4d617a29 100644 --- a/components/options_resolver.rst +++ b/components/options_resolver.rst @@ -12,10 +12,11 @@ The OptionsResolver Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/options-resolver`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/OptionsResolver). + $ composer require symfony/options-resolver + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -98,7 +99,7 @@ the ``Mailer`` class makes a mistake? .. code-block:: php $mailer = new Mailer(array( - 'usernme' => 'johndoe', // usernAme misspelled + 'usernme' => 'johndoe', // usernme misspelled (instead of username) )); No error will be shown. In the best case, the bug will appear during testing, @@ -173,7 +174,7 @@ It's a good practice to split the option configuration into a separate method:: $this->options = $resolver->resolve($options); } - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'host' => 'smtp.example.org', @@ -192,7 +193,7 @@ than processing options. Second, sub-classes may now override the // ... class GoogleMailer extends Mailer { - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { parent::configureOptions($resolver); @@ -215,7 +216,7 @@ For example, to make the ``host`` option required, you can do:: { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setRequired('host'); @@ -243,7 +244,7 @@ one required option:: { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setRequired(array('host', 'username', 'password')); @@ -263,7 +264,7 @@ retrieve the names of all required options:: // ... class GoogleMailer extends Mailer { - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { parent::configureOptions($resolver); @@ -291,7 +292,7 @@ been set:: { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setRequired('host'); @@ -301,7 +302,7 @@ been set:: // ... class GoogleMailer extends Mailer { - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { parent::configureOptions($resolver); @@ -336,7 +337,7 @@ correctly. To validate the types of the options, call { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setAllowedTypes('host', 'string'); @@ -381,7 +382,7 @@ to verify that the passed option contains one of these values:: { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setDefault('transport', 'sendmail'); @@ -403,12 +404,10 @@ is thrown:: For options with more complicated validation schemes, pass a closure which returns ``true`` for acceptable values and ``false`` for invalid values:: - $resolver->setAllowedValues(array( - // ... - $resolver->setAllowedValues('transport', function ($value) { - // return true or false - }); - )); + // ... + $resolver->setAllowedValues('transport', function ($value) { + // return true or false + }); In sub-classes, you can use :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::addAllowedValues` to add additional allowed values without erasing the ones already set. @@ -427,16 +426,18 @@ that, you can write normalizers. Normalizers are executed after validating an option. You can configure a normalizer by calling :method:`Symfony\\Component\\OptionsResolver\\OptionsResolver::setNormalizer`:: + use Symfony\Component\OptionsResolver\Options; + // ... class Mailer { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... - $resolver->setNormalizer('host', function ($options, $value) { + $resolver->setNormalizer('host', function (Options $options, $value) { if ('http://' !== substr($value, 0, 7)) { $value = 'http://'.$value; } @@ -459,11 +460,11 @@ if you need to use other options during normalization:: class Mailer { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... - $resolver->setNormalizer('host', function ($options, $value) { - if (!in_array(substr($value, 0, 7), array('http://', 'https://'))) { + $resolver->setNormalizer('host', function (Options $options, $value) { + if ('http://' !== substr($value, 0, 7) && 'https://' !== substr($value, 0, 8)) { if ('ssl' === $options['encryption']) { $value = 'https://'.$value; } else { @@ -493,7 +494,7 @@ these options, you can return the desired default value:: class Mailer { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setDefault('encryption', null); @@ -525,7 +526,7 @@ the closure:: class Mailer { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setDefaults(array( @@ -537,11 +538,11 @@ the closure:: class GoogleMailer extends Mailer { - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { parent::configureOptions($resolver); - $options->setDefault('host', function (Options $options, $previousValue) { + $resolver->setDefault('host', function (Options $options, $previousValue) { if ('ssl' === $options['encryption']) { return 'secure.example.org' } @@ -568,7 +569,7 @@ comes from the default:: class Mailer { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setDefault('port', 25); @@ -600,7 +601,7 @@ be included in the resolved options if it was actually passed to { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setDefined('port'); @@ -634,7 +635,7 @@ options in one go:: class Mailer { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... $resolver->setDefined(array('port', 'encryption')); @@ -655,7 +656,7 @@ let you find out which options are defined:: { // ... - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { parent::configureOptions($resolver); @@ -701,7 +702,7 @@ can change your code to do the configuration only once per class:: $this->options = self::$resolversByClass[$class]->resolve($options); } - protected function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver) { // ... } @@ -730,6 +731,5 @@ That's it! You now have all the tools and knowledge needed to easily process options in your code. .. _Packagist: https://packagist.org/packages/symfony/options-resolver -.. _Form component: http://symfony.com/doc/current/components/form/introduction.html .. _CHANGELOG: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/OptionsResolver/CHANGELOG.md#260 -.. _`read the Symfony 2.5 documentation`: http://symfony.com/doc/2.5/components/options_resolver.html +.. _`read the Symfony 2.5 documentation`: https://symfony.com/doc/2.5/components/options_resolver.html diff --git a/components/phpunit_bridge.rst b/components/phpunit_bridge.rst new file mode 100644 index 00000000000..1641290f2a6 --- /dev/null +++ b/components/phpunit_bridge.rst @@ -0,0 +1,166 @@ +.. index:: + single: PHPUnitBridge + single: Components; PHPUnitBridge + +The PHPUnit Bridge +================== + + The PHPUnit Bridge provides utilities to report legacy tests and usage of + deprecated code. + +It comes with the following features: + +* Forces the tests to use a consistent locale (``C``); + +* Auto-register ``class_exists`` to load Doctrine annotations (when used); + +* It displays the whole list of deprecated features used in the application; + +* Displays the stack trace of a deprecation on-demand; + +.. versionadded:: 2.7 + The PHPUnit Bridge was introduced in Symfony 2.7. It is however possible to + install the bridge in any Symfony application (even 2.3). + +Installation +------------ + +.. code-block:: terminal + + $ composer require --dev "symfony/phpunit-bridge:*" + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +.. note:: + + The PHPUnit bridge is designed to work with all maintained versions of + Symfony components, even across different major versions of them. You should + always use its very latest stable major version to get the most accurate + deprecation report. + +Usage +----- + +Once the component is installed, a ``simple-phpunit`` script is created in the +``vendor/`` directory to run tests. This script wraps the original PHPUnit binary +to provide more features: + +.. code-block:: terminal + + $ cd my-project/ + $ ./vendor/bin/simple-phpunit + +After running your PHPUnit tests, you will get a report similar to this one: + +.. image:: /_images/components/phpunit_bridge/report.png + +The summary includes: + +**Unsilenced** + Reports deprecation notices that were triggered without the recommended + `@-silencing operator`_. + +**Legacy** + Deprecation notices denote tests that explicitly test some legacy features. + +**Remaining/Other** + Deprecation notices are all other (non-legacy) notices, grouped by message, + test class and method. + +.. note:: + + If you don't want to use the ``simple-phpunit`` script, register the following + `PHPUnit event listener`_ in your PHPUnit configuration file to get the same + report about deprecations (which is created by a `PHP error handler`_ + called :class:`Symfony\\Bridge\\PhpUnit\\DeprecationErrorHandler`): + + .. code-block:: xml + + + + + + + +Trigger Deprecation Notices +--------------------------- + +Deprecation notices can be triggered by using:: + + @trigger_error('Your deprecation message', E_USER_DEPRECATED); + +Without the `@-silencing operator`_, users would need to opt-out from deprecation +notices. Silencing by default swaps this behavior and allows users to opt-in +when they are ready to cope with them (by adding a custom error handler like the +one provided by this bridge). When not silenced, deprecation notices will appear +in the **Unsilenced** section of the deprecation report. + +Mark Tests as Legacy +-------------------- + +There are three ways to mark a test as legacy: + +* (**Recommended**) Add the ``@group legacy`` annotation to its class or method; + +* Make its class name start with the ``Legacy`` prefix; + +* Make its method name start with ``testLegacy*()`` instead of ``test*()``. + +.. note:: + + If your data provider calls code that would usually trigger a deprecation, + you can prefix its name with ``provideLegacy`` or ``getLegacy`` to silent + these deprecations. If your data provider does not execute deprecated + code, it is not required to choose a special naming just because the + test being fed by the data provider is marked as legacy. + + Also be aware that choosing one of the two legacy prefixes will not mark + tests as legacy that make use of this data provider. You still have to + mark them as legacy tests explicitly. + +Configuration +------------- + +In case you need to inspect the stack trace of a particular deprecation +triggered by your unit tests, you can set the ``SYMFONY_DEPRECATIONS_HELPER`` +`environment variable`_ to a regular expression that matches this deprecation's +message, enclosed with ``/``. For example, with: + +.. code-block:: xml + + + + + + + + + + + + +PHPUnit_ will stop your test suite once a deprecation notice is triggered whose +message contains the ``"foobar"`` string. + +Making Tests Fail +----------------- + +By default, any non-legacy-tagged or any non-`@-silenced`_ deprecation notices will +make tests fail. Alternatively, setting ``SYMFONY_DEPRECATIONS_HELPER`` to an +arbitrary value (ex: ``320``) will make the tests fails only if a higher number +of deprecation notices is reached (``0`` is the default value). You can also set +the value ``"weak"`` which will make the bridge ignore any deprecation notices. +This is useful to projects that must use deprecated interfaces for backward +compatibility reasons. + +.. _PHPUnit: https://phpunit.de +.. _`PHPUnit event listener`: https://phpunit.de/manual/current/en/extending-phpunit.html#extending-phpunit.PHPUnit_Framework_TestListener +.. _`PHP error handler`: https://php.net/manual/en/book.errorfunc.php +.. _`environment variable`: https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.php-ini-constants-variables +.. _Packagist: https://packagist.org/packages/symfony/phpunit-bridge +.. _`@-silencing operator`: https://php.net/manual/en/language.operators.errorcontrol.php +.. _`@-silenced`: https://php.net/manual/en/language.operators.errorcontrol.php diff --git a/components/process.rst b/components/process.rst index 0958ed94f77..d1ce9276bf5 100644 --- a/components/process.rst +++ b/components/process.rst @@ -10,10 +10,11 @@ The Process Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/process`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Process). + $ composer require symfony/process + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -24,13 +25,14 @@ The :class:`Symfony\\Component\\Process\\Process` class allows you to execute a command in a sub-process:: use Symfony\Component\Process\Process; + use Symfony\Component\Process\Exception\ProcessFailedException; $process = new Process('ls -lsa'); $process->run(); // executes after the command finishes if (!$process->isSuccessful()) { - throw new \RuntimeException($process->getErrorOutput()); + throw new ProcessFailedException($process); } echo $process->getOutput(); @@ -42,7 +44,7 @@ The ``getOutput()`` method always returns the whole content of the standard output of the command and ``getErrorOutput()`` the content of the error output. Alternatively, the :method:`Symfony\\Component\\Process\\Process::getIncrementalOutput` and :method:`Symfony\\Component\\Process\\Process::getIncrementalErrorOutput` -methods returns the new outputs since the last call. +methods return the new output since the last call. The :method:`Symfony\\Component\\Process\\Process::clearOutput` method clears the contents of the output and @@ -63,8 +65,8 @@ with a non-zero code):: $process->mustRun(); echo $process->getOutput(); - } catch (ProcessFailedException $e) { - echo $e->getMessage(); + } catch (ProcessFailedException $exception) { + echo $exception->getMessage(); } Getting real-time Process Output @@ -113,6 +115,43 @@ are done doing other stuff:: // ... do other things + $process->wait(); + + // ... do things after the process has finished + +.. note:: + + The :method:`Symfony\\Component\\Process\\Process::wait` method is blocking, + which means that your code will halt at this line until the external + process is completed. + +.. note:: + + If a ``Response`` is sent **before** a child process had a chance to complete, + the server process will be killed (depending on your OS). It means that + your task will be stopped right away. Running an asynchronous process + is not the same as running a process that survives its parent process. + + If you want your process to survive the request/response cycle, you can + take advantage of the ``kernel.terminate`` event, and run your command + **synchronously** inside this event. Be aware that ``kernel.terminate`` + is called only if you use PHP-FPM. + +.. caution:: + + Beware also that if you do that, the said PHP-FPM process will not be + available to serve any new request until the subprocess is finished. This + means you can quickly block your FPM pool if you're not careful enough. + That is why it's generally way better not to do any fancy things even + after the request is sent, but to use a job queue instead. + +:method:`Symfony\\Component\\Process\\Process::wait` takes one optional argument: +a callback that is called repeatedly whilst the process is still running, passing +in the output and its type:: + + $process = new Process('ls -lsa'); + $process->start(); + $process->wait(function ($type, $buffer) { if (Process::ERR === $type) { echo 'ERR > '.$buffer; @@ -121,17 +160,11 @@ are done doing other stuff:: } }); -.. note:: - - The :method:`Symfony\\Component\\Process\\Process::wait` method is blocking, - which means that your code will halt at this line until the external - process is completed. - Stopping a Process ------------------ .. versionadded:: 2.3 - The ``signal`` parameter of the ``stop`` method was introduced in Symfony 2.3. + The ``signal`` parameter of the ``stop()`` method was introduced in Symfony 2.3. Any asynchronous process can be stopped at any time with the :method:`Symfony\\Component\\Process\\Process::stop` method. This method takes @@ -166,15 +199,15 @@ To make your code work better on all platforms, you might want to use the use Symfony\Component\Process\ProcessBuilder; - $builder = new ProcessBuilder(array('ls', '-lsa')); - $builder->getProcess()->run(); + $processBuilder = new ProcessBuilder(array('ls', '-lsa')); + $processBuilder->getProcess()->run(); .. versionadded:: 2.3 The :method:`ProcessBuilder::setPrefix` method was introduced in Symfony 2.3. In case you are building a binary driver, you can use the -:method:`Symfony\\Component\\Process\\Process::setPrefix` method to prefix all +:method:`Symfony\\Component\\Process\\ProcessBuilder::setPrefix` method to prefix all the generated process commands. The following example will generate two process commands for a tar binary @@ -182,17 +215,17 @@ adapter:: use Symfony\Component\Process\ProcessBuilder; - $builder = new ProcessBuilder(); - $builder->setPrefix('/usr/bin/tar'); + $processBuilder = new ProcessBuilder(); + $processBuilder->setPrefix('/usr/bin/tar'); // '/usr/bin/tar' '--list' '--file=archive.tar.gz' - echo $builder + echo $processBuilder ->setArguments(array('--list', '--file=archive.tar.gz')) ->getProcess() ->getCommandLine(); // '/usr/bin/tar' '-xzf' 'archive.tar.gz' - echo $builder + echo $processBuilder ->setArguments(array('-xzf', 'archive.tar.gz')) ->getProcess() ->getCommandLine(); @@ -210,7 +243,7 @@ timeout (in seconds):: $process->run(); If the timeout is reached, a -:class:`Symfony\\Process\\Exception\\RuntimeException` is thrown. +:class:`Symfony\\Component\\Process\\Exception\\RuntimeException` is thrown. For long running commands, it is your responsibility to perform the timeout check regularly:: @@ -235,12 +268,12 @@ Process Idle Timeout In contrast to the timeout of the previous paragraph, the idle timeout only considers the time since the last output was produced by the process:: - use Symfony\Component\Process\Process; + use Symfony\Component\Process\Process; - $process = new Process('something-with-variable-runtime'); - $process->setTimeout(3600); - $process->setIdleTimeout(60); - $process->run(); + $process = new Process('something-with-variable-runtime'); + $process->setTimeout(3600); + $process->setIdleTimeout(60); + $process->run(); In the case above, a process is considered timed out, when either the total runtime exceeds 3600 seconds, or the process does not produce any output for 60 seconds. @@ -249,7 +282,7 @@ Process Signals --------------- .. versionadded:: 2.3 - The ``signal`` method was introduced in Symfony 2.3. + The ``signal()`` method was introduced in Symfony 2.3. When running a program asynchronously, you can send it POSIX signals with the :method:`Symfony\\Component\\Process\\Process::signal` method:: @@ -275,12 +308,10 @@ Process Pid ----------- .. versionadded:: 2.3 - The ``getPid`` method was introduced in Symfony 2.3. + The ``getPid()`` method was introduced in Symfony 2.3. You can access the `pid`_ of a running process with the -:method:`Symfony\\Component\\Process\\Process::getPid` method. - -.. code-block:: php +:method:`Symfony\\Component\\Process\\Process::getPid` method:: use Symfony\Component\Process\Process; @@ -311,16 +342,29 @@ Use :method:`Symfony\\Component\\Process\\Process::disableOutput` and .. caution:: - You can not enable or disable the output while the process is running. + You cannot enable or disable the output while the process is running. + + If you disable the output, you cannot access ``getOutput()``, + ``getIncrementalOutput()``, ``getErrorOutput()`` or ``getIncrementalErrorOutput()``. + Moreover, you could not pass a callback to the ``start()``, ``run()`` or ``mustRun()`` + methods or use ``setIdleTimeout()``. + +Finding the Executable PHP Binary +--------------------------------- + +This component also provides a utility class called +:class:`Symfony\\Component\\Process\\PhpExecutableFinder` which returns the +absolute path of the executable PHP binary available on your server:: + + use Symfony\Component\Process\PhpExecutableFinder; - If you disable the output, you cannot access ``getOutput``, - ``getIncrementalOutput``, ``getErrorOutput`` or ``getIncrementalErrorOutput``. - Moreover, you could not pass a callback to the ``start``, ``run`` or ``mustRun`` - methods or use ``setIdleTimeout``. + $phpBinaryFinder = new PhpExecutableFinder(); + $phpBinaryPath = $phpBinaryFinder->find(); + // $phpBinaryPath = '/usr/local/bin/php' (the result will be different on your computer) .. _`Symfony Issue#5759`: https://github.com/symfony/symfony/issues/5759 .. _`PHP Bug#39992`: https://bugs.php.net/bug.php?id=39992 -.. _`exec`: http://en.wikipedia.org/wiki/Exec_(operating_system) -.. _`pid`: http://en.wikipedia.org/wiki/Process_identifier -.. _`PHP Documentation`: http://php.net/manual/en/pcntl.constants.php +.. _`exec`: https://en.wikipedia.org/wiki/Exec_(operating_system) +.. _`pid`: https://en.wikipedia.org/wiki/Process_identifier +.. _`PHP Documentation`: https://php.net/manual/en/pcntl.constants.php .. _Packagist: https://packagist.org/packages/symfony/process diff --git a/components/property_access/introduction.rst b/components/property_access.rst similarity index 62% rename from components/property_access/introduction.rst rename to components/property_access.rst index d678cbb2509..f229d52d4a2 100644 --- a/components/property_access/introduction.rst +++ b/components/property_access.rst @@ -11,10 +11,11 @@ The PropertyAccess Component Installation ------------ -You can install the component in two different ways: +.. code-block:: terminal -* :doc:`Install it via Composer` (``symfony/property-access`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/PropertyAccess). + $ composer require symfony/property-access + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -29,7 +30,7 @@ default configuration:: use Symfony\Component\PropertyAccess\PropertyAccess; - $accessor = PropertyAccess::createPropertyAccessor(); + $propertyAccessor = PropertyAccess::createPropertyAccessor(); .. versionadded:: 2.3 The :method:`Symfony\\Component\\PropertyAccess\\PropertyAccess::createPropertyAccessor` @@ -47,8 +48,8 @@ method. This is done using the index notation that is used in PHP:: 'first_name' => 'Wouter', ); - var_dump($accessor->getValue($person, '[first_name]')); // 'Wouter' - var_dump($accessor->getValue($person, '[age]')); // null + var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($person, '[age]')); // null As you can see, the method will return ``null`` if the index does not exists. @@ -64,13 +65,13 @@ You can also use multi dimensional arrays:: ) ); - var_dump($accessor->getValue($persons, '[0][first_name]')); // 'Wouter' - var_dump($accessor->getValue($persons, '[1][first_name]')); // 'Ryan' + var_dump($propertyAccessor->getValue($persons, '[0][first_name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($persons, '[1][first_name]')); // 'Ryan' Reading from Objects -------------------- -The ``getValue`` method is a very robust method, and you can see all of its +The ``getValue()`` method is a very robust method, and you can see all of its features when working with objects. Accessing public Properties @@ -82,13 +83,13 @@ To read from properties, use the "dot" notation:: $person = new Person(); $person->firstName = 'Wouter'; - var_dump($accessor->getValue($person, 'firstName')); // 'Wouter' + var_dump($propertyAccessor->getValue($person, 'firstName')); // 'Wouter' $child = new Person(); $child->firstName = 'Bar'; $person->children = array($child); - var_dump($accessor->getValue($person, 'children[0].firstName')); // 'Bar' + var_dump($propertyAccessor->getValue($person, 'children[0].firstName')); // 'Bar' .. caution:: @@ -100,10 +101,10 @@ To read from properties, use the "dot" notation:: Using Getters ~~~~~~~~~~~~~ -The ``getValue`` method also supports reading using getters. The method will +The ``getValue()`` method also supports reading using getters. The method will be created using common naming conventions for getters. It camelizes the property name (``first_name`` becomes ``FirstName``) and prefixes it with -``get``. So the actual method becomes ``getFirstName``:: +``get``. So the actual method becomes ``getFirstName()``:: // ... class Person @@ -118,7 +119,7 @@ property name (``first_name`` becomes ``FirstName``) and prefixes it with $person = new Person(); - var_dump($accessor->getValue($person, 'first_name')); // 'Wouter' + var_dump($propertyAccessor->getValue($person, 'first_name')); // 'Wouter' Using Hassers/Issers ~~~~~~~~~~~~~~~~~~~~ @@ -146,10 +147,10 @@ getters, this means that you can do something like this:: $person = new Person(); - if ($accessor->getValue($person, 'author')) { + if ($propertyAccessor->getValue($person, 'author')) { var_dump('He is an author'); } - if ($accessor->getValue($person, 'children')) { + if ($propertyAccessor->getValue($person, 'children')) { var_dump('He has children'); } @@ -158,7 +159,7 @@ This will produce: ``He is an author`` Magic ``__get()`` Method ~~~~~~~~~~~~~~~~~~~~~~~~ -The ``getValue`` method can also use the magic ``__get`` method:: +The ``getValue()`` method can also use the magic ``__get()`` method:: // ... class Person @@ -175,14 +176,14 @@ The ``getValue`` method can also use the magic ``__get`` method:: $person = new Person(); - var_dump($accessor->getValue($person, 'Wouter')); // array(...) + var_dump($propertyAccessor->getValue($person, 'Wouter')); // array(...) .. _components-property-access-magic-call: Magic ``__call()`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~ -At last, ``getValue`` can use the magic ``__call`` method, but you need to +At last, ``getValue()`` can use the magic ``__call()`` method, but you need to enable this feature by using :class:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder`:: // ... @@ -208,19 +209,19 @@ enable this feature by using :class:`Symfony\\Component\\PropertyAccess\\Propert $person = new Person(); - // Enable magic __call - $accessor = PropertyAccess::createPropertyAccessorBuilder() + // enables PHP __call() magic method + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->enableMagicCall() ->getPropertyAccessor(); - var_dump($accessor->getValue($person, 'wouter')); // array(...) + var_dump($propertyAccessor->getValue($person, 'wouter')); // array(...) .. versionadded:: 2.3 The use of magic ``__call()`` method was introduced in Symfony 2.3. .. caution:: - The ``__call`` feature is disabled by default, you can enable it by calling + The ``__call()`` feature is disabled by default, you can enable it by calling :method:`PropertyAccessorBuilder::enableMagicCall` see `Enable other Features`_. @@ -235,17 +236,17 @@ method:: // ... $person = array(); - $accessor->setValue($person, '[first_name]', 'Wouter'); + $propertyAccessor->setValue($person, '[first_name]', 'Wouter'); - var_dump($accessor->getValue($person, '[first_name]')); // 'Wouter' + var_dump($propertyAccessor->getValue($person, '[first_name]')); // 'Wouter' // or // var_dump($person['first_name']); // 'Wouter' Writing to Objects ------------------ -The ``setValue`` method has the same features as the ``getValue`` method. You -can use setters, the magic ``__set`` method or properties to set values:: +The ``setValue()`` method has the same features as the ``getValue()`` method. You +can use setters, the magic ``__set()`` method or properties to set values:: // ... class Person @@ -259,25 +260,33 @@ can use setters, the magic ``__set`` method or properties to set values:: $this->lastName = $name; } + public function getLastName() + { + return $this->lastName; + } + + public function getChildren() + { + return $this->children; + } + public function __set($property, $value) { $this->$property = $value; } - - // ... } $person = new Person(); - $accessor->setValue($person, 'firstName', 'Wouter'); - $accessor->setValue($person, 'lastName', 'de Jong'); - $accessor->setValue($person, 'children', array(new Person())); + $propertyAccessor->setValue($person, 'firstName', 'Wouter'); + $propertyAccessor->setValue($person, 'lastName', 'de Jong'); // setLastName is called + $propertyAccessor->setValue($person, 'children', array(new Person())); // __set is called var_dump($person->firstName); // 'Wouter' var_dump($person->getLastName()); // 'de Jong' - var_dump($person->children); // array(Person()); + var_dump($person->getChildren()); // array(Person()); -You can also use ``__call`` to set values but you need to enable the feature, +You can also use ``__call()`` to set values but you need to enable the feature, see `Enable other Features`_. .. code-block:: php @@ -305,14 +314,59 @@ see `Enable other Features`_. $person = new Person(); // Enable magic __call - $accessor = PropertyAccess::createPropertyAccessorBuilder() + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->enableMagicCall() ->getPropertyAccessor(); - $accessor->setValue($person, 'wouter', array(...)); + $propertyAccessor->setValue($person, 'wouter', array(...)); var_dump($person->getWouter()); // array(...) +Writing to Array Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``PropertyAccessor`` class allows to update the content of arrays stored in +properties through *adder* and *remover* methods. + +.. code-block:: php + + // ... + class Person + { + /** + * @var string[] + */ + private $children = array(); + + public function getChildren(): array + { + return $this->children; + } + + public function addChild(string $name): void + { + $this->children[$name] = $name; + } + + public function removeChild(string $name): void + { + unset($this->children[$name]); + } + } + + $person = new Person(); + $propertyAccessor->setValue($person, 'children', array('kevin', 'wouter')); + + var_dump($person->getChildren()); // array('kevin', 'wouter') + +The PropertyAccess component checks for methods called ``add()`` +and ``remove()``. Both methods must be defined. +For instance, in the previous example, the component looks for the ``addChild()`` +and ``removeChild()`` methods to access to the ``children`` property. +`The Inflector component`_ is used to find the singular of a property name. + +If available, *adder* and *remover* methods have priority over a *setter* method. + Checking Property Paths ----------------------- @@ -324,7 +378,7 @@ instead:: $person = new Person(); - if ($accessor->isReadable($person, 'firstName')) { + if ($propertyAccessor->isReadable($person, 'firstName')) { // ... } @@ -335,7 +389,7 @@ method to find out whether a property path can be updated:: $person = new Person(); - if ($accessor->isWritable($person, 'firstName')) { + if ($propertyAccessor->isWritable($person, 'firstName')) { // ... } @@ -363,13 +417,13 @@ You can also mix objects and arrays:: $person = new Person(); - $accessor->setValue($person, 'children[0]', new Person); + $propertyAccessor->setValue($person, 'children[0]', new Person); // equal to $person->getChildren()[0] = new Person() - $accessor->setValue($person, 'children[0].firstName', 'Wouter'); + $propertyAccessor->setValue($person, 'children[0].firstName', 'Wouter'); // equal to $person->getChildren()[0]->firstName = 'Wouter' - var_dump('Hello '.$accessor->getValue($person, 'children[0].firstName')); // 'Wouter' + var_dump('Hello '.$propertyAccessor->getValue($person, 'children[0].firstName')); // 'Wouter' // equal to $person->getChildren()[0]->firstName Enable other Features @@ -380,29 +434,29 @@ configured to enable extra features. To do that you could use the :class:`Symfony\\Component\\PropertyAccess\\PropertyAccessorBuilder`:: // ... - $accessorBuilder = PropertyAccess::createPropertyAccessorBuilder(); + $propertyAccessorBuilder = PropertyAccess::createPropertyAccessorBuilder(); - // Enable magic __call - $accessorBuilder->enableMagicCall(); + // enables magic __call + $propertyAccessorBuilder->enableMagicCall(); - // Disable magic __call - $accessorBuilder->disableMagicCall(); + // disables magic __call + $propertyAccessorBuilder->disableMagicCall(); - // Check if magic __call handling is enabled - $accessorBuilder->isMagicCallEnabled(); // true or false + // checks if magic __call handling is enabled + $propertyAccessorBuilder->isMagicCallEnabled(); // true or false // At the end get the configured property accessor - $accessor = $accessorBuilder->getPropertyAccessor(); + $propertyAccessor = $propertyAccessorBuilder->getPropertyAccessor(); // Or all in one - $accessor = PropertyAccess::createPropertyAccessorBuilder() + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->enableMagicCall() ->getPropertyAccessor(); Or you can pass parameters directly to the constructor (not the recommended way):: // ... - $accessor = new PropertyAccessor(true); // this enables handling of magic __call - + $propertyAccessor = new PropertyAccessor(true); // this enables handling of magic __call .. _Packagist: https://packagist.org/packages/symfony/property-access +.. _The Inflector component: https://github.com/symfony/inflector diff --git a/components/property_access/index.rst b/components/property_access/index.rst deleted file mode 100644 index 106405c95ee..00000000000 --- a/components/property_access/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -PropertyAccess -============== - -.. toctree:: - :maxdepth: 2 - - introduction diff --git a/cookbook/psr7.rst b/components/psr7.rst similarity index 73% rename from cookbook/psr7.rst rename to components/psr7.rst index 79cab6caa09..851d4c93e41 100644 --- a/cookbook/psr7.rst +++ b/components/psr7.rst @@ -4,22 +4,25 @@ The PSR-7 Bridge ================ - The PSR-7 bridge converts :doc:`HttpFoundation ` + The PSR-7 bridge converts :doc:`HttpFoundation ` objects from and to objects implementing HTTP message interfaces defined by the `PSR-7`_. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/psr-http-message-bridge`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/psr-http-message-bridge). + $ composer require symfony/psr-http-message-bridge + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc The bridge also needs a PSR-7 implementation to allow converting HttpFoundation objects to PSR-7 objects. It provides native support for `Zend Diactoros`_. -Use Composer (``zendframework/zend-diactoros`` on `Packagist`_) or refer to -the project documentation to install it. +Use Composer (`zendframework/zend-diactoros on Packagist `_) +or refer to the project documentation to install it. Usage ----- @@ -33,8 +36,8 @@ that builds objects implementing PSR-7 interfaces from HttpFoundation objects. It also provide a default implementation using Zend Diactoros internally. The following code snippet explain how to convert a :class:`Symfony\\Component\\HttpFoundation\\Request` -to a Zend Diactoros :class:`Zend\\Diactoros\\ServerRequest` implementing the -:class:`Psr\\Http\\Message\\ServerRequestInterface` interface:: +to a ``Zend\\Diactoros\\ServerRequest`` class implementing the +``Psr\\Http\\Message\\ServerRequestInterface`` interface:: use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; use Symfony\Component\HttpFoundation\Request; @@ -45,9 +48,9 @@ to a Zend Diactoros :class:`Zend\\Diactoros\\ServerRequest` implementing the $psr7Factory = new DiactorosFactory(); $psrRequest = $psr7Factory->createRequest($symfonyRequest); -And now from a :class:`Symfony\\Component\\HttpFoundation\\Response` to a Zend -Diactoros :class:`Zend\\Diactoros\\Response` implementing the :class:`Psr\\Http\\Message\\ResponseInterface` -interface:: +And now from a :class:`Symfony\\Component\\HttpFoundation\\Response` to a +``Zend\\Diactoros\\Response`` class implementing the +``Psr\\Http\\Message\\ResponseInterface`` interface:: use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; use Symfony\Component\HttpFoundation\Response; @@ -64,8 +67,9 @@ On the other hand, the bridge provide a factory interface called :class:`Symfony\\Bridge\\PsrHttpMessage\\HttpFoundationFactoryInterface` that builds HttpFoundation objects from objects implementing PSR-7 interfaces. -The next snippet explain how to convert an object implementing the :class:`Psr\\Http\\Message\\ServerRequestInterface` -interface to a :class:`Symfony\\Component\\HttpFoundation\\Request` instance:: +The next snippet explain how to convert an object implementing the +``Psr\\Http\\Message\\ServerRequestInterface`` interface to a +:class:`Symfony\\Component\\HttpFoundation\\Request` instance:: use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; @@ -74,7 +78,7 @@ interface to a :class:`Symfony\\Component\\HttpFoundation\\Request` instance:: $httpFoundationFactory = new HttpFoundationFactory(); $symfonyRequest = $httpFoundationFactory->createRequest($psrRequest); -From an object implementing the :class:`Psr\\Http\\Message\\ResponseInterface` +From an object implementing the ``Psr\\Http\\Message\\ResponseInterface`` to a :class:`Symfony\\Component\\HttpFoundation\\Response` instance:: use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; @@ -84,6 +88,6 @@ to a :class:`Symfony\\Component\\HttpFoundation\\Response` instance:: $httpFoundationFactory = new HttpFoundationFactory(); $symfonyResponse = $httpFoundationFactory->createResponse($psrResponse); -.. _`PSR-7`: http://www.php-fig.org/psr/psr-7/ -.. _Packagist: https://packagist.org/packages/symfony/psr-http-message-bridge +.. _`PSR-7`: https://www.php-fig.org/psr/psr-7/ .. _`Zend Diactoros`: https://github.com/zendframework/zend-diactoros +.. _`symfony/psr-http-message-bridge on Packagist`: https://packagist.org/packages/symfony/psr-http-message-bridge diff --git a/components/require_autoload.rst.inc b/components/require_autoload.rst.inc index 568562507c1..9d47bd7ffca 100644 --- a/components/require_autoload.rst.inc +++ b/components/require_autoload.rst.inc @@ -1,3 +1,6 @@ -Then, require the ``vendor/autoload.php`` file to enable the autoloading mechanism -provided by Composer. Otherwise, your application won't be able to find the classes -of this Symfony component. +.. note:: + + If you install this component outside of a Symfony application, you must + require the ``vendor/autoload.php`` file in your code to enable the class + autoloading mechanism provided by Composer. Read + :doc:`this article ` for more details. diff --git a/components/routing/introduction.rst b/components/routing.rst similarity index 75% rename from components/routing/introduction.rst rename to components/routing.rst index 69467b4dbfd..b1690cce6bf 100644 --- a/components/routing/introduction.rst +++ b/components/routing.rst @@ -5,16 +5,17 @@ The Routing Component ===================== - The Routing component maps an HTTP request to a set of configuration - variables. + The Routing component maps an HTTP request to a set of configuration + variables. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/routing`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Routing). + $ composer require symfony/routing + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -35,23 +36,22 @@ your autoloader to load the Routing component:: use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; - $route = new Route('/foo', array('controller' => 'MyController')); + $route = new Route('/foo', array('_controller' => 'MyController')); $routes = new RouteCollection(); $routes->add('route_name', $route); - $context = new RequestContext($_SERVER['REQUEST_URI']); + $context = new RequestContext('/'); $matcher = new UrlMatcher($routes, $context); $parameters = $matcher->match('/foo'); - // array('controller' => 'MyController', '_route' => 'route_name') + // array('_controller' => 'MyController', '_route' => 'route_name') .. note:: - Be careful when using ``$_SERVER['REQUEST_URI']``, as it may include - any query parameters on the URL, which will cause problems with route - matching. An easy way to solve this is to use the HttpFoundation component - as explained :ref:`below `. + The :class:`Symfony\\Component\\Routing\\RequestContext` parameters can be populated + with the values stored in ``$_SERVER``, but it's easier to use the HttpFoundation + component as explained :ref:`below `. You can add as many routes as you like to a :class:`Symfony\\Component\\Routing\\RouteCollection`. @@ -63,11 +63,15 @@ URL path and some array of custom variables in its constructor. This array of custom variables can be *anything* that's significant to your application, and is returned when that route is matched. -If no matching route can be found a -:class:`Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException` will be thrown. +The :method:`UrlMatcher::match() ` +returns the variables you set on the route as well as the wildcard placeholders +(see below). Your application can now use this information to continue +processing the request. In addition to the configured variables, a ``_route`` +key is added, which holds the name of the matched route. -In addition to your array of custom variables, a ``_route`` key is added, -which holds the name of the matched route. +If no matching route can be found, a +:class:`Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException` will +be thrown. Defining Routes ~~~~~~~~~~~~~~~ @@ -88,7 +92,7 @@ A full route definition can contain up to seven parts: are the least commonly needed. #. A host. This is matched against the host of the request. See - :doc:`/components/routing/hostname_pattern` for more details. + :doc:`/routing/hostname_pattern` for more details. #. An array of schemes. These enforce a certain HTTP scheme (``http``, ``https``). @@ -97,33 +101,37 @@ A full route definition can contain up to seven parts: Take the following route, which combines several of these ideas:: - $route = new Route( - '/archive/{month}', // path - array('controller' => 'showArchive'), // default values - array('month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'), // requirements - array(), // options - '{subdomain}.example.com', // host - array(), // schemes - array() // methods - ); - - // ... - - $parameters = $matcher->match('/archive/2012-01'); - // array( - // 'controller' => 'showArchive', - // 'month' => '2012-01', - // 'subdomain' => 'www', - // '_route' => ... - // ) - - $parameters = $matcher->match('/archive/foo'); - // throws ResourceNotFoundException + $route = new Route( + '/archive/{month}', // path + array('_controller' => 'showArchive'), // default values + array('month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'), // requirements + array(), // options + '{subdomain}.example.com', // host + array(), // schemes + array() // methods + ); + + // ... + + $parameters = $matcher->match('/archive/2012-01'); + // array( + // '_controller' => 'showArchive', + // 'month' => '2012-01', + // 'subdomain' => 'www', + // '_route' => ... + // ) + + $parameters = $matcher->match('/archive/foo'); + // throws ResourceNotFoundException In this case, the route is matched by ``/archive/2012-01``, because the ``{month}`` wildcard matches the regular expression wildcard given. However, ``/archive/foo`` does *not* match, because "foo" fails the month wildcard. +When using wildcards, these are returned in the array result when calling +``match``. The part of the path that the wildcard matched (e.g. ``2012-01``) is used +as value. + .. tip:: If you want to match all URLs which start with a certain path and end in an @@ -181,8 +189,8 @@ with this class via its constructor:: .. _components-routing-http-foundation: Normally you can pass the values from the ``$_SERVER`` variable to populate the -:class:`Symfony\\Component\\Routing\\RequestContext`. But If you use the -:doc:`HttpFoundation ` component, you can use its +:class:`Symfony\\Component\\Routing\\RequestContext`. But if you use the +:doc:`HttpFoundation ` component, you can use its :class:`Symfony\\Component\\HttpFoundation\\Request` class to feed the :class:`Symfony\\Component\\Routing\\RequestContext` in a shortcut:: @@ -199,11 +207,14 @@ to find a route that fits the given request you can also build a URL from a certain route:: use Symfony\Component\Routing\Generator\UrlGenerator; + use Symfony\Component\Routing\RequestContext; + use Symfony\Component\Routing\Route; + use Symfony\Component\Routing\RouteCollection; $routes = new RouteCollection(); $routes->add('show_post', new Route('/show/{slug}')); - $context = new RequestContext($_SERVER['REQUEST_URI']); + $context = new RequestContext('/'); $generator = new UrlGenerator($routes, $context); @@ -251,10 +262,10 @@ To load this file, you can use the following code. This assumes that your use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\Loader\YamlFileLoader; - // look inside *this* directory - $locator = new FileLocator(array(__DIR__)); - $loader = new YamlFileLoader($locator); - $collection = $loader->load('routes.yml'); + // looks inside *this* directory + $fileLocator = new FileLocator(array(__DIR__)); + $loader = new YamlFileLoader($fileLocator); + $routes = $loader->load('routes.yml'); Besides :class:`Symfony\\Component\\Routing\\Loader\\YamlFileLoader` there are two other loaders that work the same way: @@ -269,14 +280,14 @@ have to provide the name of a PHP file which returns a :class:`Symfony\\Componen use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; - $collection = new RouteCollection(); - $collection->add( + $routes = new RouteCollection(); + $routes->add( 'route_name', - new Route('/foo', array('controller' => 'ExampleController')) + new Route('/foo', array('_controller' => 'ExampleController')) ); // ... - return $collection; + return $routes; Routes as Closures .................. @@ -291,7 +302,7 @@ calls a closure and uses the result as a :class:`Symfony\\Component\\Routing\\Ro }; $loader = new ClosureLoader(); - $collection = $loader->load($closure); + $routes = $loader->load($closure); Routes as Annotations ..................... @@ -302,6 +313,8 @@ Last but not least there are route definitions from class annotations. The specific details are left out here. +.. include:: /_includes/_annotation_loader_tip.rst.inc + The all-in-one Router ~~~~~~~~~~~~~~~~~~~~~ @@ -314,7 +327,7 @@ a path to the main route definition and some other settings:: $resource, array $options = array(), RequestContext $context = null, - array $defaults = array() + LoggerInterface $logger = null ); With the ``cache_dir`` option you can enable route caching (if you provide a @@ -322,11 +335,11 @@ path) or disable caching (if it's set to ``null``). The caching is done automatically in the background if you want to use it. A basic example of the :class:`Symfony\\Component\\Routing\\Router` class would look like:: - $locator = new FileLocator(array(__DIR__)); - $requestContext = new RequestContext($_SERVER['REQUEST_URI']); + $fileLocator = new FileLocator(array(__DIR__)); + $requestContext = new RequestContext('/'); $router = new Router( - new YamlFileLoader($locator), + new YamlFileLoader($fileLocator), 'routes.yml', array('cache_dir' => __DIR__.'/cache'), $requestContext @@ -339,4 +352,17 @@ automatically in the background if you want to use it. A basic example of the are saved in the ``cache_dir``. This means your script must have write permissions for that location. +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /routing + /routing/* + /controller + /controller/* + /configuration/apache_router + .. _Packagist: https://packagist.org/packages/symfony/routing diff --git a/components/routing/index.rst b/components/routing/index.rst deleted file mode 100644 index b7f4d40386b..00000000000 --- a/components/routing/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Routing -======= - -.. toctree:: - :maxdepth: 2 - - introduction - hostname_pattern diff --git a/components/security/introduction.rst b/components/security.rst similarity index 76% rename from components/security/introduction.rst rename to components/security.rst index ad6a44ae692..2d20b6408df 100644 --- a/components/security/introduction.rst +++ b/components/security.rst @@ -14,10 +14,11 @@ The Security Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/security`` on Packagist_); -* Use the official Git repository (https://github.com/symfony/Security). + $ composer require symfony/security + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -38,13 +39,18 @@ used separately: ``symfony/security-acl`` It provides a fine grained permissions mechanism based on Access Control Lists. -Sections --------- +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: -* :doc:`/components/security/firewall` -* :doc:`/components/security/authentication` -* :doc:`/components/security/authorization` -* :doc:`/components/security/secure_tools` + /components/security/* + /security + /security/* + /reference/configuration/security + /reference/constraints/UserPassword .. _Packagist: https://packagist.org/packages/symfony/security .. _`CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery diff --git a/components/security/authentication.rst b/components/security/authentication.rst index ce7dd20c259..b3a0aa67583 100644 --- a/components/security/authentication.rst +++ b/components/security/authentication.rst @@ -15,7 +15,7 @@ firewall map is able to extract the user's credentials from the current a token, containing these credentials. The next thing the listener should do is ask the authentication manager to validate the given token, and return an *authenticated* token if the supplied credentials were found to be valid. -The listener should then store the authenticated token using +The listener should then store the authenticated token using :class:`the token storage `:: use Symfony\Component\Security\Http\Firewall\ListenerInterface; @@ -76,6 +76,7 @@ The default authentication manager is an instance of :class:`Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationProviderManager`:: use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; + use Symfony\Component\Security\Core\Exception\AuthenticationException; // instances of Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface $providers = array(...); @@ -85,7 +86,7 @@ The default authentication manager is an instance of try { $authenticatedToken = $authenticationManager ->authenticate($unauthenticatedToken); - } catch (AuthenticationException $failed) { + } catch (AuthenticationException $exception) { // authentication failed } @@ -107,7 +108,7 @@ Each provider (since it implements has a method :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::supports` by which the ``AuthenticationProviderManager`` can determine if it supports the given token. If this is the case, the -manager then calls the provider's method :class:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::authenticate`. +manager then calls the provider's method :method:`Symfony\\Component\\Security\\Core\\Authentication\\Provider\\AuthenticationProviderInterface::authenticate`. This method should return an authenticated token or throw an :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException` (or any other exception extending it). @@ -145,20 +146,20 @@ password was valid:: ) ); - // for some extra checks: is account enabled, locked, expired, etc.? + // for some extra checks: is account enabled, locked, expired, etc. $userChecker = new UserChecker(); // an array of password encoders (see below) $encoderFactory = new EncoderFactory(...); - $provider = new DaoAuthenticationProvider( + $daoProvider = new DaoAuthenticationProvider( $userProvider, $userChecker, 'secured_area', $encoderFactory ); - $provider->authenticate($unauthenticatedToken); + $daoProvider->authenticate($unauthenticatedToken); .. note:: @@ -177,19 +178,19 @@ user. This allows you to use different encoding strategies for different types of users. The default :class:`Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactory` receives an array of encoders:: + use Acme\Entity\LegacyUser; use Symfony\Component\Security\Core\Encoder\EncoderFactory; use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; + use Symfony\Component\Security\Core\User\User; $defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000); $weakEncoder = new MessageDigestPasswordEncoder('md5', true, 1); $encoders = array( - 'Symfony\\Component\\Security\\Core\\User\\User' => $defaultEncoder, - 'Acme\\Entity\\LegacyUser' => $weakEncoder, - + User::class => $defaultEncoder, + LegacyUser::class => $weakEncoder, // ... ); - $encoderFactory = new EncoderFactory($encoders); Each encoder should implement :class:`Symfony\\Component\\Security\\Core\\Encoder\\PasswordEncoderInterface` @@ -234,6 +235,7 @@ own, it just needs to follow these rules: } // ... + } } Using Password Encoders @@ -252,7 +254,7 @@ which should be used to encode this user's password:: $encoder = $encoderFactory->getEncoder($user); - // will return $weakEncoder (see above) + // returns $weakEncoder (see above) $encodedPassword = $encoder->encodePassword($plainPassword, $user->getSalt()); $user->setPassword($encodedPassword); @@ -274,5 +276,53 @@ in) is correct, you can use:: $user->getSalt() ); +Authentication Events +--------------------- + +The security component provides 4 related authentication events: + +=============================== ================================================ ============================================================================== +Name Event Constant Argument Passed to the Listener +=============================== ================================================ ============================================================================== +security.authentication.success ``AuthenticationEvents::AUTHENTICATION_SUCCESS`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationEvent` +security.authentication.failure ``AuthenticationEvents::AUTHENTICATION_FAILURE`` :class:`Symfony\\Component\\Security\\Core\\Event\\AuthenticationFailureEvent` +security.interactive_login ``SecurityEvents::INTERACTIVE_LOGIN`` :class:`Symfony\\Component\\Security\\Http\\Event\\InteractiveLoginEvent` +security.switch_user ``SecurityEvents::SWITCH_USER`` :class:`Symfony\\Component\\Security\\Http\\Event\\SwitchUserEvent` +=============================== ================================================ ============================================================================== + +Authentication Success and Failure Events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a provider authenticates the user, a ``security.authentication.success`` +event is dispatched. But beware - this event will fire, for example, on *every* +request if you have session-based authentication. See ``security.interactive_login`` +below if you need to do something when a user *actually* logs in. + +When a provider attempts authentication but fails (i.e. throws an ``AuthenticationException``), +a ``security.authentication.failure`` event is dispatched. You could listen on +the ``security.authentication.failure`` event, for example, in order to log +failed login attempts. + +Security Events +~~~~~~~~~~~~~~~ + +The ``security.interactive_login`` event is triggered after a user has actively +logged into your website. It is important to distinguish this action from +non-interactive authentication methods, such as: + +* authentication based on your session. +* authentication using a HTTP basic or HTTP digest header. + +You could listen on the ``security.interactive_login`` event, for example, in +order to give your user a welcome flash message every time they log in. + +The ``security.switch_user`` event is triggered every time you activate +the ``switch_user`` firewall listener. + +.. seealso:: + + For more information on switching users, see + :doc:`/security/impersonating_user`. + .. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form .. _`BasePasswordEncoder::checkPasswordLength`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php diff --git a/components/security/authorization.rst b/components/security/authorization.rst index 8ffeefc3b5f..93dd80a7293 100644 --- a/components/security/authorization.rst +++ b/components/security/authorization.rst @@ -46,7 +46,7 @@ the votes (either positive, negative or neutral) it has received. It recognizes several strategies: ``affirmative`` (default) - grant access as soon as any voter returns an affirmative response; + grant access as soon as there is one voter granting access; ``consensus`` grant access if there are more voters granting access than there are denying; @@ -118,11 +118,10 @@ on a "remember-me" cookie, or even authenticated anonymously? .. code-block:: php use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; + use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; + use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; - $anonymousClass = 'Symfony\Component\Security\Core\Authentication\Token\AnonymousToken'; - $rememberMeClass = 'Symfony\Component\Security\Core\Authentication\Token\RememberMeToken'; - - $trustResolver = new AuthenticationTrustResolver($anonymousClass, $rememberMeClass); + $trustResolver = new AuthenticationTrustResolver(AnonymousToken::class, RememberMeToken::class); $authenticatedVoter = new AuthenticatedVoter($trustResolver); @@ -132,7 +131,7 @@ on a "remember-me" cookie, or even authenticated anonymously? // any object $object = ...; - $vote = $authenticatedVoter->vote($token, $object, array('IS_AUTHENTICATED_FULLY'); + $vote = $authenticatedVoter->vote($token, $object, array('IS_AUTHENTICATED_FULLY')); RoleVoter ~~~~~~~~~ @@ -182,7 +181,7 @@ Roles Roles are objects that give expression to a certain right the user has. The only requirement is that they implement :class:`Symfony\\Component\\Security\\Core\\Role\\RoleInterface`, -which means they should also have a :method:`Symfony\\Component\\Security\\Core\\Role\\Role\\RoleInterface::getRole` +which means they should also have a :method:`Symfony\\Component\\Security\\Core\\Role\\RoleInterface::getRole` method that returns a string representation of the role itself. The default :class:`Symfony\\Component\\Security\\Core\\Role\\Role` simply returns its first constructor argument:: @@ -191,7 +190,7 @@ first constructor argument:: $role = new Role('ROLE_ADMIN'); - // will show 'ROLE_ADMIN' + // shows 'ROLE_ADMIN' var_dump($role->getRole()); .. note:: diff --git a/components/security/firewall.rst b/components/security/firewall.rst index 64603efb319..a4672901266 100644 --- a/components/security/firewall.rst +++ b/components/security/firewall.rst @@ -61,7 +61,7 @@ the user:: use Symfony\Component\HttpFoundation\RequestMatcher; use Symfony\Component\Security\Http\Firewall\ExceptionListener; - $map = new FirewallMap(); + $firewallMap = new FirewallMap(); $requestMatcher = new RequestMatcher('^/secured-area/'); @@ -70,7 +70,7 @@ the user:: $exceptionListener = new ExceptionListener(...); - $map->add($requestMatcher, $listeners, $exceptionListener); + $firewallMap->add($requestMatcher, $listeners, $exceptionListener); The firewall map will be given to the firewall as its first argument, together with the event dispatcher that is used by the :class:`Symfony\\Component\\HttpKernel\\HttpKernel`:: @@ -81,7 +81,7 @@ with the event dispatcher that is used by the :class:`Symfony\\Component\\HttpKe // the EventDispatcher used by the HttpKernel $dispatcher = ...; - $firewall = new Firewall($map, $dispatcher); + $firewall = new Firewall($firewallMap, $dispatcher); $dispatcher->addListener( KernelEvents::REQUEST, diff --git a/components/security/index.rst b/components/security/index.rst deleted file mode 100644 index e9fa2c24b14..00000000000 --- a/components/security/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Security -======== - -.. toctree:: - :maxdepth: 2 - - introduction - firewall - authentication - authorization - secure_tools \ No newline at end of file diff --git a/components/security/secure_tools.rst b/components/security/secure_tools.rst index a1279040d24..a7060c26597 100644 --- a/components/security/secure_tools.rst +++ b/components/security/secure_tools.rst @@ -1,10 +1,16 @@ -Securely Comparing Strings and Generating Random Numbers -======================================================== +Securely Comparing Strings and Generating Random Values +======================================================= The Symfony Security component comes with a collection of nice utilities related to security. These utilities are used by Symfony, but you should also use them if you want to solve the problem they address. +.. note:: + + The functions described in this article were introduced in PHP 5.6 or 7. + For older PHP versions, a polyfill is provided by the + `Symfony Polyfill Component`_. + Comparing Strings ~~~~~~~~~~~~~~~~~ @@ -12,59 +18,40 @@ The time it takes to compare two strings depends on their differences. This can be used by an attacker when the two strings represent a password for instance; it is known as a `Timing attack`_. -Internally, when comparing two passwords, Symfony uses a constant-time -algorithm; you can use the same strategy in your own code thanks to the -:class:`Symfony\\Component\\Security\\Core\\Util\\StringUtils` class:: - - use Symfony\Component\Security\Core\Util\StringUtils; +When comparing two passwords, you should use the :phpfunction:`hash_equals` +function:: - // is some known string (e.g. password) equal to some user input? - $bool = StringUtils::equals($knownString, $userInput); + if (hash_equals($knownString, $userInput)) { + // ... + } -.. caution:: - - To avoid timing attacks, the known string must be the first argument - and the user-entered string the second. - -Generating a Secure random Number +Generating a Secure Random String ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Whenever you need to generate a secure random number, you are highly -encouraged to use the Symfony -:class:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom` class:: - - use Symfony\Component\Security\Core\Util\SecureRandom; +Whenever you need to generate a secure random string, you are highly +encouraged to use the :phpfunction:`random_bytes` function:: - $generator = new SecureRandom(); - $random = $generator->nextBytes(10); + $random = random_bytes(10); -The -:method:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom::nextBytes` -method returns a random string composed of the number of characters passed as -an argument (10 in the above example). +The function returns a random string, suitable for cryptographic use, of +the number bytes passed as an argument (10 in the above example). -The SecureRandom class works better when OpenSSL is installed. But when it's -not available, it falls back to an internal algorithm, which needs a seed file -to work correctly. Just pass a file name to enable it:: - - use Symfony\Component\Security\Core\Util\SecureRandom; - - $generator = new SecureRandom('/some/path/to/store/the/seed.txt'); +.. tip:: - $random = $generator->nextBytes(10); - $hashedRandom = md5($random); // see tip below + The ``random_bytes()`` function returns a binary string which may contain + the ``\0`` character. This can cause trouble in several common scenarios, + such as storing this value in a database or including it as part of the + URL. The solution is to encode or hash the value returned by + ``random_bytes()`` (to do that, you can use a simple ``base64_encode()`` + PHP function). -.. note:: - - If you're using the Symfony Framework, you can get a secure random number - generator via the ``security.secure_random`` service. +Generating a Secure Random Number +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. tip:: +If you need to generate a cryptographically secure random integer, you should +use the :phpfunction:`random_int` function:: - The ``nextBytes()`` method returns a binary string which may contain the - ``\0`` character. This can cause trouble in several common scenarios, such - as storing this value in a database or including it as part of the URL. The - solution is to hash the value returned by ``nextBytes()`` (to do that, you - can use a simple ``md5()`` PHP function). + $random = random_int(1, 10); -.. _`Timing attack`: http://en.wikipedia.org/wiki/Timing_attack +.. _`Timing attack`: https://en.wikipedia.org/wiki/Timing_attack +.. _`Symfony Polyfill Component`: https://github.com/symfony/polyfill diff --git a/components/serializer.rst b/components/serializer.rst index 235ce8acf58..3c50d1b193c 100644 --- a/components/serializer.rst +++ b/components/serializer.rst @@ -5,8 +5,8 @@ The Serializer Component ======================== - The Serializer component is meant to be used to turn objects into a - specific format (XML, JSON, YAML, ...) and the other way around. + The Serializer component is meant to be used to turn objects into a + specific format (XML, JSON, YAML, ...) and the other way around. In order to do so, the Serializer component follows the following simple schema. @@ -14,29 +14,28 @@ simple schema. .. _component-serializer-encoders: .. _component-serializer-normalizers: -.. image:: /images/components/serializer/serializer_workflow.png +.. image:: /_images/components/serializer/serializer_workflow.png -As you can see in the picture above, an array is used as a man in -the middle. This way, Encoders will only deal with turning specific -**formats** into **arrays** and vice versa. The same way, Normalizers +As you can see in the picture above, an array is used as an intermediary between +objects and serialized contents. This way, encoders will only deal with turning +specific **formats** into **arrays** and vice versa. The same way, Normalizers will deal with turning specific **objects** into **arrays** and vice versa. -Serialization is a complicated topic, and while this component may not work -in all cases, it can be a useful tool while developing tools to serialize -and deserialize your objects. +Serialization is a complex topic. This component may not cover all your use cases out of the box, +but it can be useful for developing tools to serialize and deserialize your objects. Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/serializer`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Serializer). + $ composer require symfony/serializer +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc -To use the ``ObjectNormalizer``, the :doc:`PropertyAccess component ` +To use the ``ObjectNormalizer``, the :doc:`PropertyAccess component ` must also be installed. Usage @@ -44,7 +43,7 @@ Usage Using the Serializer component is really simple. You just need to set up the :class:`Symfony\\Component\\Serializer\\Serializer` specifying -which Encoders and Normalizer are going to be available:: +which encoders and normalizer are going to be available:: use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Encoder\XmlEncoder; @@ -57,10 +56,9 @@ which Encoders and Normalizer are going to be available:: $serializer = new Serializer($normalizers, $encoders); The preferred normalizer is the -:class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer`, but other -normalizers are available. -To read more about them, refer to the `Normalizers`_ section of this page. All -the examples shown below use the ``ObjectNormalizer``. +:class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer`, +but other normalizers are available. All the examples shown below use +the ``ObjectNormalizer``. Serializing an Object --------------------- @@ -68,13 +66,14 @@ Serializing an Object For the sake of this example, assume the following class already exists in your project:: - namespace Acme; + namespace App\Model; class Person { private $age; private $name; - private $sportsman; + private $sportsperson; + private $createdAt; // Getters public function getName() @@ -87,10 +86,15 @@ exists in your project:: return $this->age; } + public function getCreatedAt() + { + return $this->createdAt; + } + // Issers - public function isSportsman() + public function isSportsperson() { - return $this->sportsman; + return $this->sportsperson; } // Setters @@ -104,23 +108,28 @@ exists in your project:: $this->age = $age; } - public function setSportsman($sportsman) + public function setSportsperson($sportsperson) + { + $this->sportsperson = $sportsperson; + } + + public function setCreatedAt($createdAt) { - $this->sportsman = $sportsman; + $this->createdAt = $createdAt; } } Now, if you want to serialize this object into JSON, you only need to use the Serializer service created before:: - $person = new Acme\Person(); + $person = new App\Model\Person(); $person->setName('foo'); $person->setAge(99); - $person->setSportsman(false); + $person->setSportsperson(false); $jsonContent = $serializer->serialize($person, 'json'); - // $jsonContent contains {"name":"foo","age":99,"sportsman":false} + // $jsonContent contains {"name":"foo","age":99,"sportsperson":false} echo $jsonContent; // or return it in a Response @@ -134,15 +143,17 @@ Deserializing an Object You'll now learn how to do the exact opposite. This time, the information of the ``Person`` class would be encoded in XML format:: + use App\Model\Person; + $data = << foo 99 - false + false EOF; - $person = $serializer->deserialize($data, 'Acme\Person', 'xml'); + $person = $serializer->deserialize($data, Person::class, 'xml'); In this case, :method:`Symfony\\Component\\Serializer\\Serializer::deserialize` needs three parameters: @@ -156,10 +167,11 @@ Deserializing in an Existing Object The serializer can also be used to update an existing object:: - $person = new Acme\Person(); + // ... + $person = new Person(); $person->setName('bar'); $person->setAge(99); - $person->setSportsman(true); + $person->setSportsperson(true); $data = << @@ -168,8 +180,8 @@ The serializer can also be used to update an existing object:: EOF; - $serializer->deserialize($data, 'Acme\Person', 'xml', array('object_to_populate' => $person)); - // $obj2 = Acme\Person(name: 'foo', age: '99', sportsman: true) + $serializer->deserialize($data, Person::class, 'xml', array('object_to_populate' => $person)); + // $person = App\Model\Person(name: 'foo', age: '69', sportsperson: true) This is a common need when working with an ORM. @@ -300,7 +312,7 @@ You are now able to serialize only attributes in the groups you want:: $serializer = new Serializer(array($normalizer)); $data = $serializer->normalize($obj, null, array('groups' => array('group1'))); - // $data = ['foo' => 'foo']; + // $data = array('foo' => 'foo'); $obj2 = $serializer->denormalize( array('foo' => 'foo', 'bar' => 'bar'), @@ -310,6 +322,8 @@ You are now able to serialize only attributes in the groups you want:: ); // $obj2 = MyObj(foo: 'foo', bar: 'bar') +.. include:: /_includes/_annotation_loader_tip.rst.inc + .. _ignoring-attributes-when-serializing: Ignoring Attributes @@ -342,7 +356,7 @@ method on the normalizer definition:: $encoder = new JsonEncoder(); $serializer = new Serializer(array($normalizer), array($encoder)); - $serializer->serialize($person, 'json'); // Output: {"name":"foo","sportsman":false} + $serializer->serialize($person, 'json'); // Output: {"name":"foo","sportsperson":false} Converting Property Names when Serializing and Deserializing ------------------------------------------------------------ @@ -361,8 +375,8 @@ Given you have the following object:: class Company { - public name; - public address; + public $name; + public $address; } And in the serialized form, all attributes must be prefixed by ``org_`` like @@ -383,17 +397,17 @@ A custom name converter can handle such cases:: public function denormalize($propertyName) { - // remove org_ prefix + // removes 'org_' prefix return 'org_' === substr($propertyName, 0, 4) ? substr($propertyName, 4) : $propertyName; } } -The custom normalizer can be used by passing it as second parameter of any +The custom name converter can be used by passing it as second parameter of any class extending :class:`Symfony\\Component\\Serializer\\Normalizer\\AbstractNormalizer`, including :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` and :class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer`:: - use Symfony\Component\Serializer\Encoder\JsonEncoder + use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -406,9 +420,9 @@ and :class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer`:: $obj->name = 'Acme Inc.'; $obj->address = '123 Main Street, Big City'; - $json = $serializer->serialize($obj); + $json = $serializer->serialize($obj, 'json'); // {"org_name": "Acme Inc.", "org_address": "123 Main Street, Big City"} - $objCopy = $serializer->deserialize($json); + $objCopy = $serializer->deserialize($json, Company::class, 'json'); // Same data as $obj .. _using-camelized-method-names-for-underscored-attributes: @@ -421,8 +435,9 @@ CamelCase to snake_case interface was introduced in Symfony 2.7. In many formats, it's common to use underscores to separate words (also known -as snake_case). However, PSR-1 specifies that the preferred style for PHP -properties and methods is CamelCase. +as snake_case). However, in Symfony applications is common to use CamelCase to +name properties (even though the `PSR-1 standard`_ doesn't recommend any +specific case for property names). Symfony provides a built-in name converter designed to transform between snake_case and CamelCased styles during serialization and deserialization @@ -459,8 +474,8 @@ Serializing Boolean Attributes ------------------------------ If you are using isser methods (methods prefixed by ``is``, like -``Acme\Person::isSportsman()``), the Serializer component will automatically -detect and use it to serialize related attributes. +``App\Model\Person::isSportsperson()``), the Serializer component will +automatically detect and use it to serialize related attributes. The ``ObjectNormalizer`` also takes care of methods starting with ``has``, ``add`` and ``remove``. @@ -470,7 +485,7 @@ Using Callbacks to Serialize Properties with Object Instances When serializing, you can set a callback to format a specific object property:: - use Acme\Person; + use App\Model\Person; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Serializer; @@ -502,25 +517,27 @@ Normalizers There are several types of normalizers available: :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` - This normalizer leverages the :doc:`PropertyAccess Component ` + This normalizer leverages the :doc:`PropertyAccess Component ` to read and write in the object. It means that it can access to properties directly and through getters, setters, hassers, adders and removers. It supports calling the constructor during the denormalization process. - Objects are normalized to a map of property names (method name stripped of - the "get"/"set"/"has"/"remove" prefix and converted to lower case) to property - values. + Objects are normalized to a map of property names and values (names are + generated removing the ``get``, ``set``, ``has`` or ``remove`` prefix from + the method name and lowercasing the first letter; e.g. ``getFirstName()`` -> + ``firstName``). - The ``ObjectNormalizer`` is the most powerful normalizer. It is a configured - by default when using the Symfony Standard Edition with the serializer enabled. + The ``ObjectNormalizer`` is the most powerful normalizer. It is configured by + default when using the Symfony Standard Edition with the serializer enabled. :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` This normalizer reads the content of the class by calling the "getters" (public methods starting with "get"). It will denormalize data by calling the constructor and the "setters" (public methods starting with "set"). - Objects are normalized to a map of property names (method name stripped of - the "get" prefix and converted to lower case) to property values. + Objects are normalized to a map of property names and values (names are + generated removing the ``get`` prefix from the method name and lowercasing + the first letter; e.g. ``getFirstName()`` -> ``firstName``). :class:`Symfony\\Component\\Serializer\\Normalizer\\PropertyNormalizer` This normalizer directly reads and writes public properties as well as @@ -599,19 +616,20 @@ Circular references are common when dealing with entity relations:: } To avoid infinite loops, :class:`Symfony\\Component\\Serializer\\Normalizer\\GetSetMethodNormalizer` -throws a :class:`Symfony\\Component\\Serializer\\Exception\\CircularReferenceException` +or :class:`Symfony\\Component\\Serializer\\Normalizer\\ObjectNormalizer` +throw a :class:`Symfony\\Component\\Serializer\\Exception\\CircularReferenceException` when such a case is encountered:: $member = new Member(); $member->setName('Kévin'); - $org = new Organization(); - $org->setName('Les-Tilleuls.coop'); - $org->setMembers(array($member)); + $organization = new Organization(); + $organization->setName('Les-Tilleuls.coop'); + $organization->setMembers(array($member)); - $member->setOrganization($org); + $member->setOrganization($organization); - echo $serializer->serialize($org, 'json'); // Throws a CircularReferenceException + echo $serializer->serialize($organization, 'json'); // Throws a CircularReferenceException The ``setCircularReferenceLimit()`` method of this normalizer sets the number of times it will serialize the same object before considering it a circular @@ -632,10 +650,20 @@ having unique identifiers:: var_dump($serializer->serialize($org, 'json')); // {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]} +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /serializer + .. seealso:: A popular alternative to the Symfony Serializer Component is the third-party library, `JMS serializer`_ (released under the Apache license, so incompatible with GPLv2 projects). +.. _`PSR-1 standard`: https://www.php-fig.org/psr/psr-1/ .. _`JMS serializer`: https://github.com/schmittjoh/serializer .. _Packagist: https://packagist.org/packages/symfony/serializer diff --git a/components/stopwatch.rst b/components/stopwatch.rst index bde4a7393a2..878a1275a1f 100644 --- a/components/stopwatch.rst +++ b/components/stopwatch.rst @@ -10,10 +10,11 @@ The Stopwatch Component Installation ------------ -You can install the component in two different ways: +.. code-block:: terminal -* :doc:`Install it via Composer` (``symfony/stopwatch`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Stopwatch). + $ composer require symfony/stopwatch + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -28,16 +29,16 @@ microtime by yourself. Instead, use the simple use Symfony\Component\Stopwatch\Stopwatch; $stopwatch = new Stopwatch(); - // Start event named 'eventName' + // starts event named 'eventName' $stopwatch->start('eventName'); // ... some code goes here $event = $stopwatch->stop('eventName'); The :class:`Symfony\\Component\\Stopwatch\\StopwatchEvent` object can be retrieved -from the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::start`, -:method:`Symfony\\Component\\Stopwatch\\Stopwatch::stop`, -:method:`Symfony\\Component\\Stopwatch\\Stopwatch::lap` and -:method:`Symfony\\Component\\Stopwatch\\Stopwatch::getEvent` methods. +from the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::start`, +:method:`Symfony\\Component\\Stopwatch\\Stopwatch::stop`, +:method:`Symfony\\Component\\Stopwatch\\Stopwatch::lap` and +:method:`Symfony\\Component\\Stopwatch\\Stopwatch::getEvent` methods. The latter should be used when you need to retrieve the duration of an event while it is still running. @@ -57,7 +58,7 @@ This is exactly what the :method:`Symfony\\Component\\Stopwatch\\Stopwatch::lap` method does:: $stopwatch = new Stopwatch(); - // Start event named 'foo' + // starts event named 'foo' $stopwatch->start('foo'); // ... some code goes here $stopwatch->lap('foo'); @@ -74,13 +75,13 @@ call:: In addition to periods, you can get other useful information from the event object. For example:: - $event->getCategory(); // Returns the category the event was started in - $event->getOrigin(); // Returns the event start time in milliseconds - $event->ensureStopped(); // Stops all periods not already stopped - $event->getStartTime(); // Returns the start time of the very first period - $event->getEndTime(); // Returns the end time of the very last period - $event->getDuration(); // Returns the event duration, including all periods - $event->getMemory(); // Returns the max memory usage of all periods + $event->getCategory(); // returns the category the event was started in + $event->getOrigin(); // returns the event start time in milliseconds + $event->ensureStopped(); // stops all periods not already stopped + $event->getStartTime(); // returns the start time of the very first period + $event->getEndTime(); // returns the end time of the very last period + $event->getDuration(); // returns the event duration, including all periods + $event->getMemory(); // returns the max memory usage of all periods Sections -------- diff --git a/components/templating/introduction.rst b/components/templating.rst similarity index 91% rename from components/templating/introduction.rst rename to components/templating.rst index 00539d1ed98..3549fc50020 100644 --- a/components/templating/introduction.rst +++ b/components/templating.rst @@ -16,10 +16,11 @@ The Templating Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/templating`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Templating). + $ composer require symfony/templating + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -38,9 +39,9 @@ which uses the template reference to actually find and load the template:: use Symfony\Component\Templating\TemplateNameParser; use Symfony\Component\Templating\Loader\FilesystemLoader; - $loader = new FilesystemLoader(__DIR__.'/views/%name%'); + $filesystemLoader = new FilesystemLoader(__DIR__.'/views/%name%'); - $templating = new PhpEngine(new TemplateNameParser(), $loader); + $templating = new PhpEngine(new TemplateNameParser(), $filesystemLoader); echo $templating->render('hello.php', array('firstname' => 'Fabien')); @@ -73,7 +74,7 @@ Including Templates The best way to share a snippet of template code is to create a template that can then be included by other templates. As the ``$view`` variable is an -instance of ``PhpEngine``, you can use the ``render`` method (which was used +instance of ``PhpEngine``, you can use the ``render()`` method (which was used to render the template originally) inside the template to render another template:: @@ -141,8 +142,8 @@ The Templating component can be easily extended via helpers. Helpers are PHP obj provide features useful in a template context. The component has 2 built-in helpers: -* :doc:`/components/templating/helpers/assetshelper` -* :doc:`/components/templating/helpers/slotshelper` +* :doc:`/components/templating/assetshelper` +* :doc:`/components/templating/slotshelper` Before you can use these helpers, you need to register them using :method:`Symfony\\Component\\Templating\\PhpEngine::set`:: @@ -188,9 +189,7 @@ takes a list of engines and acts just like a normal templating engine. The only difference is that it delegates the calls to one of the other engines. To choose which one to use for the template, the :method:`EngineInterface::supports() ` -method is used. - -.. code-block:: php +method is used:: use Acme\Templating\CustomEngine; use Symfony\Component\Templating\PhpEngine; @@ -201,4 +200,15 @@ method is used. new CustomEngine(...), )); +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/templating/* + /templating + /templating/* + .. _Packagist: https://packagist.org/packages/symfony/templating diff --git a/components/templating/helpers/assetshelper.rst b/components/templating/assetshelper.rst similarity index 83% rename from components/templating/helpers/assetshelper.rst rename to components/templating/assetshelper.rst index 837b94255bc..f5a4700cfe9 100644 --- a/components/templating/helpers/assetshelper.rst +++ b/components/templating/assetshelper.rst @@ -9,9 +9,9 @@ generating asset paths: .. code-block:: html+php - + - + The assets helper can then be configured to render paths to a CDN or modify the paths in case your assets live in a sub-directory of your host (e.g. ``http://example.com/app``). @@ -32,10 +32,10 @@ Now, if you use the helper, everything will be prefixed with ``/foo/bar``: .. code-block:: html+php - - + + Absolute Urls ------------- @@ -51,15 +51,15 @@ You can also use the third argument of the helper to force an absolute URL: .. code-block:: html+php - - + + .. note:: If you already set a URL in the constructor, using the third argument of - ``getUrl`` will not affect the generated URL. + ``getUrl()`` will not affect the generated URL. Versioning ---------- @@ -82,10 +82,10 @@ fourth argument of the helper: .. code-block:: html+php - - + + Multiple Packages ----------------- diff --git a/components/templating/helpers/index.rst b/components/templating/helpers/index.rst deleted file mode 100644 index 11665e89274..00000000000 --- a/components/templating/helpers/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. index:: - single: Templating; Templating Helpers - -The Templating Helpers -====================== - -.. toctree:: - :hidden: - - slotshelper - assetshelper - -The Templating component comes with some useful helpers. These helpers contain -functions to ease some common tasks. - -.. include:: map.rst.inc diff --git a/components/templating/helpers/map.rst.inc b/components/templating/helpers/map.rst.inc deleted file mode 100644 index 7af793aaa1c..00000000000 --- a/components/templating/helpers/map.rst.inc +++ /dev/null @@ -1,2 +0,0 @@ -* :doc:`/components/templating/helpers/slotshelper` -* :doc:`/components/templating/helpers/assetshelper` diff --git a/components/templating/index.rst b/components/templating/index.rst deleted file mode 100644 index 6db2575e8f6..00000000000 --- a/components/templating/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Templating -========== - -.. toctree:: - :maxdepth: 2 - - introduction - helpers/index diff --git a/components/templating/helpers/slotshelper.rst b/components/templating/slotshelper.rst similarity index 100% rename from components/templating/helpers/slotshelper.rst rename to components/templating/slotshelper.rst diff --git a/components/translation/introduction.rst b/components/translation.rst similarity index 92% rename from components/translation/introduction.rst rename to components/translation.rst index 383245c3e25..c90e668551a 100644 --- a/components/translation/introduction.rst +++ b/components/translation.rst @@ -11,10 +11,11 @@ The Translation Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/translation`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/Translation). + $ composer require symfony/translation + +Alternatively, you can clone the ``_ repository. .. include:: /components/require_autoload.rst.inc @@ -29,14 +30,11 @@ catalogs*). Configuration ~~~~~~~~~~~~~ -The constructor of the ``Translator`` class needs one argument: The locale. - -.. code-block:: php +The constructor of the ``Translator`` class needs one argument: The locale:: use Symfony\Component\Translation\Translator; - use Symfony\Component\Translation\MessageSelector; - $translator = new Translator('fr_FR', new MessageSelector()); + $translator = new Translator('fr_FR'); .. note:: @@ -90,9 +88,9 @@ Loader too. The default loaders are: * :class:`Symfony\\Component\\Translation\\Loader\\JsonFileLoader` - to load catalogs from JSON files. * :class:`Symfony\\Component\\Translation\\Loader\\YamlFileLoader` - to load - catalogs from Yaml files (requires the :doc:`Yaml component`). + catalogs from Yaml files (requires the :doc:`Yaml component`). -All file loaders require the :doc:`Config component `. +All file loaders require the :doc:`Config component `. You can also :doc:`create your own Loader `, in case the format is not already supported by one of the default loaders. @@ -111,7 +109,7 @@ Loading Messages with the ``ArrayLoader`` Loading messages can be done by calling :method:`Symfony\\Component\\Translation\\Translator::addResource`. The first -argument is the loader name (this was the first argument of the ``addLoader`` +argument is the loader name (this was the first argument of the ``addLoader()`` method), the second is the resource and the third argument is the locale:: // ... @@ -122,7 +120,7 @@ method), the second is the resource and the third argument is the locale:: Loading Messages with the File Loaders ...................................... -If you use one of the file loaders, you should also use the ``addResource`` +If you use one of the file loaders, you should also use the ``addResource()`` method. The only difference is that you should put the file name to the resource file as the second argument, instead of an array:: @@ -212,6 +210,18 @@ Usage Read how to use the Translation component in :doc:`/components/translation/usage`. +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/translation/* + /translation + /translation/* + /validation/translations + .. _Packagist: https://packagist.org/packages/symfony/translation -.. _`ISO 3166-1 alpha-2`: http://en.wikipedia.org/wiki/ISO_3166-1#Current_codes -.. _`ISO 639-1`: http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +.. _`ISO 3166-1 alpha-2`: https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes +.. _`ISO 639-1`: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes diff --git a/components/translation/custom_formats.rst b/components/translation/custom_formats.rst index 14c6e65c8b0..79088b5e168 100644 --- a/components/translation/custom_formats.rst +++ b/components/translation/custom_formats.rst @@ -46,10 +46,10 @@ create the catalog that will be returned:: } } - $catalogue = new MessageCatalogue($locale); - $catalogue->add($messages, $domain); + $messageCatalogue = new MessageCatalogue($locale); + $messageCatalogue->add($messages, $domain); - return $catalogue; + return $messageCatalogue; } } @@ -112,8 +112,8 @@ YAML file are dumped into a text file with the custom format:: use Symfony\Component\Translation\Loader\YamlFileLoader; $loader = new YamlFileLoader(); - $catalogue = $loader->load(__DIR__ . '/translations/messages.fr_FR.yml' , 'fr_FR'); + $translations = $loader->load(__DIR__ . '/translations/messages.fr_FR.yml' , 'fr_FR'); $dumper = new MyFormatDumper(); - $dumper->dump($catalogue, array('path' => __DIR__.'/dumps')); + $dumper->dump($translations, array('path' => __DIR__.'/dumps')); diff --git a/components/translation/index.rst b/components/translation/index.rst deleted file mode 100644 index c50e43f2be7..00000000000 --- a/components/translation/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Translation -=========== - -.. toctree:: - :maxdepth: 2 - - introduction - usage - custom_formats diff --git a/components/translation/usage.rst b/components/translation/usage.rst index b1553bfa1f1..99add24390e 100644 --- a/components/translation/usage.rst +++ b/components/translation/usage.rst @@ -12,7 +12,7 @@ Imagine you want to translate the string *"Symfony is great"* into French:: $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array( - 'Symfony is great!' => 'J\'aime Symfony!', + 'Symfony is great!' => 'Symfony est super !', ), 'fr_FR'); var_dump($translator->trans('Symfony is great!')); @@ -115,11 +115,11 @@ recommended format. These files are parsed by one of the loader classes. - + Symfony is great J'aime Symfony - + symfony.great J'aime Symfony @@ -139,6 +139,8 @@ recommended format. These files are parsed by one of the loader classes. 'symfony.great' => 'J\'aime Symfony', ); +.. _translation-real-vs-keyword-messages: + .. sidebar:: Using Real or Keyword Messages This example illustrates the two different philosophies when creating @@ -326,8 +328,8 @@ effect after removing the explicit rules: '{0} There are no apples|[20,Inf[ There are many apples|There is one apple|a_few: There are %count% apples' For example, for ``1`` apple, the standard rule ``There is one apple`` will -be used. For ``2-19`` apples, the second standard rule ``There are %count% -apples`` will be selected. +be used. For ``2-19`` apples, the second standard rule +``There are %count% apples`` will be selected. An :class:`Symfony\\Component\\Translation\\Interval` can represent a finite set of numbers: @@ -369,9 +371,6 @@ use for translation:: 'fr_FR' ); -.. _`L10n`: http://en.wikipedia.org/wiki/Internationalization_and_localization -.. _`ISO 31-11`: http://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals - Retrieving the Message Catalogue -------------------------------- @@ -392,3 +391,6 @@ The ``$messages`` variable will have the following structure:: 'Value is too long' => 'Valeur est trop long', ), ); + +.. _`L10n`: https://en.wikipedia.org/wiki/Internationalization_and_localization +.. _`ISO 31-11`: https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals diff --git a/components/using_components.rst b/components/using_components.rst index 3cb2eab270d..5c84ba3be91 100644 --- a/components/using_components.rst +++ b/components/using_components.rst @@ -22,7 +22,7 @@ Using the Finder Component **2.** Open a terminal and use Composer to grab the library. -.. code-block:: bash +.. code-block:: terminal $ composer require symfony/finder @@ -31,7 +31,7 @@ whatever component you want. .. tip:: - `Install composer`_ if you don't have it already present on your system. + `Install Composer`_ if you don't have it already present on your system. Depending on how you install, you may end up with a ``composer.phar`` file in your directory. In that case, no worries! Just run ``php composer.phar require symfony/finder``. @@ -62,7 +62,7 @@ Using all of the Components If you want to use all of the Symfony Components, then instead of adding them one by one, you can include the ``symfony/symfony`` package: -.. code-block:: bash +.. code-block:: terminal $ composer require symfony/symfony @@ -78,4 +78,4 @@ documentation to find out more about how to use it. And have fun! .. _Composer: https://getcomposer.org -.. _Install composer: https://getcomposer.org/download/ +.. _Install Composer: https://getcomposer.org/download/ diff --git a/components/validator.rst b/components/validator.rst new file mode 100644 index 00000000000..fbb951479ba --- /dev/null +++ b/components/validator.rst @@ -0,0 +1,79 @@ +.. index:: + single: Validator + single: Components; Validator + +The Validator Component +======================= + + The Validator component provides tools to validate values following the + `JSR-303 Bean Validation specification`_. + +Installation +------------ + +.. code-block:: terminal + + $ composer require symfony/validator + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +The Validator component behavior is based on two concepts: + +* Constraints, which define the rules to be validated; +* Validators, which are the classes that contain the actual validation logic. + +The following example shows how to validate that a string is at least 10 +characters long:: + + use Symfony\Component\Validator\Validation; + use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Constraints\NotBlank; + + $validator = Validation::createValidator(); + $violations = $validator->validate('Bernhard', array( + new Length(array('min' => 10)), + new NotBlank(), + )); + + if (0 !== count($violations)) { + // there are errors, now you can show them + foreach ($violations as $violation) { + echo $violation->getMessage().'
'; + } + } + +The validator returns the list of violations. + +Retrieving a Validator Instance +------------------------------- + +The :class:`Symfony\\Component\\Validator\\Validator` class is the main access +point of the Validator component. To create a new instance of this class, it's +recommended to use the :class:`Symfony\\Component\\Validator\\Validation` class:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidator(); + +This ``$validator`` object can validate simple variables such as strings, numbers +and arrays, but it can't validate objects. To do so, configure the +``Validator`` class as explained in the next sections. + +Learn More +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /components/validator/* + /validation + /validation/* + +.. _`JSR-303 Bean Validation specification`: http://jcp.org/en/jsr/detail?id=303 +.. _Packagist: https://packagist.org/packages/symfony/validator diff --git a/components/validator/metadata.rst b/components/validator/metadata.rst new file mode 100755 index 00000000000..15b3cd56658 --- /dev/null +++ b/components/validator/metadata.rst @@ -0,0 +1,72 @@ +.. index:: + single: Validator; Metadata + +Metadata +======== + +The :class:`Symfony\\Component\\Validator\\Mapping\\ClassMetadata` class +represents and manages all the configured constraints on a given class. + +Properties +---------- + +The Validator component can validate public, protected or private properties. +The following example shows how to validate that the ``$firstName`` property of +the ``Author`` class has at least 3 characters:: + + // ... + use Symfony\Component\Validator\Mapping\ClassMetadata; + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + private $firstName; + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); + $metadata->addPropertyConstraint( + 'firstName', + new Assert\Length(array("min" => 3)) + ); + } + } + +Getters +------- + +Constraints can also be applied to the value returned by any public *getter* +method, which are the methods whose names start with ``get``, ``has`` or ``is``. +This feature allows to validate your objects dynamically. + +Suppose that, for security reasons, you want to validate that a password field +doesn't match the first name of the user. First, create a public method called +``isPasswordSafe()`` to define this custom validation logic:: + + public function isPasswordSafe() + { + return $this->firstName !== $this->password; + } + +Then, add the Validator component configuration to the class:: + + // ... + use Symfony\Component\Validator\Mapping\ClassMetadata; + use Symfony\Component\Validator\Constraints as Assert; + + class Author + { + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addGetterConstraint('passwordSafe', new Assert\IsTrue(array( + 'message' => 'The password cannot match your first name', + ))); + } + } + +Classes +------- + +Some constraints allow to validate the entire object. For example, the +:doc:`Callback ` constraint is a generic +constraint that's applied to the class itself. diff --git a/components/validator/resources.rst b/components/validator/resources.rst new file mode 100644 index 00000000000..c32e35c7b17 --- /dev/null +++ b/components/validator/resources.rst @@ -0,0 +1,202 @@ +.. index:: + single: Validator; Loading Resources + +Loading Resources +================= + +The Validator component uses metadata to validate a value. This metadata defines +how a class, array or any other value should be validated. When validating a +class, the metadata is defined by the class itself. When validating simple values, +the metadata must be passed to the validation methods. + +Class metadata can be defined in a configuration file or in the class itself. +The Validator component collects that metadata using a set of loaders. + +.. seealso:: + + You'll learn how to define the metadata in :doc:`metadata`. + +The StaticMethodLoader +---------------------- + +The most basic loader is the +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\StaticMethodLoader`. +This loader gets the metadata by calling a static method of the class. The name +of the method is configured using the +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::addMethodMapping` +method of the validator builder:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->addMethodMapping('loadValidatorMetadata') + ->getValidator(); + +In this example, the validation metadata is retrieved executing the +``loadValidatorMetadata()`` method of the class:: + + use Symfony\Component\Validator\Mapping\ClassMetadata; + use Symfony\Component\Validator\Constraints as Assert; + + class User + { + protected $name; + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('name', new Assert\NotBlank()); + $metadata->addPropertyConstraint('name', new Assert\Length(array( + 'min' => 5, + 'max' => 20, + ))); + } + } + +.. tip:: + + Instead of calling ``addMethodMapping()`` multiple times to add several + method names, you can also use + :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addMethodMappings` + to set an array of supported method names. + +The File Loaders +---------------- + +The component also provides two file loaders, one to load YAML files and one to +load XML files. Use +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::addYamlMapping` or +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::addXmlMapping` to +configure the locations of these files:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->addYamlMapping('config/validation.yml') + ->getValidator(); + +.. note:: + + If you want to load YAML mapping files then you will also need to install + :doc:`the Yaml component `. + +.. tip:: + + Just like with the method mappings, you can also use + :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addYamlMappings` and + :method:`Symfony\\Component\\Validator\\ValidatorBuilder::addXmlMappings` + to configure an array of file paths. + +The AnnotationLoader +-------------------- + +At last, the component provides an +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\AnnotationLoader` to get +the metadata from the annotations of the class. Annotations are defined as ``@`` +prefixed classes included in doc block comments (``/** ... */``). For example:: + + use Symfony\Component\Validator\Constraints as Assert; + // ... + + class User + { + /** + * @Assert\NotBlank() + */ + protected $name; + } + +To enable the annotation loader, call the +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::enableAnnotationMapping` +method. It takes an optional annotation reader instance, which defaults to +``Doctrine\Common\Annotations\AnnotationReader``:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->enableAnnotationMapping() + ->getValidator(); + +To disable the annotation loader after it was enabled, call +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::disableAnnotationMapping`. + +.. include:: /_includes/_annotation_loader_tip.rst.inc + +Using Multiple Loaders +---------------------- + +The component provides a +:class:`Symfony\\Component\\Validator\\Mapping\\Loader\\LoaderChain` class to +execute several loaders sequentially in the same order they were defined: + +The ``ValidatorBuilder`` will already take care of this when you configure +multiple mappings:: + + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->enableAnnotationMapping() + ->addMethodMapping('loadValidatorMetadata') + ->addXmlMapping('config/validation.xml') + ->getValidator(); + +Caching +------- + +Using many loaders to load metadata from different places is convenient, but it +can slow down your application because each file needs to be parsed, validated +and converted into a :class:`Symfony\\Component\\Validator\\Mapping\\ClassMetadata` +instance. To solve this problem, you can cache the ``ClassMetadata`` information. + +The Validator component comes with an +:class:`Symfony\\Component\\Validator\\Mapping\\Cache\\ApcCache` +implementation. You can easily create other cachers by creating a class which +implements :class:`Symfony\\Component\\Validator\\Mapping\\Cache\\CacheInterface`. + +.. note:: + + The loaders already use a singleton load mechanism. That means that the + loaders will only load and parse a file once and put that in a property, + which will then be used the next time it is asked for metadata. However, + the Validator still needs to merge all metadata of one class from every + loader when it is requested. + +Enable the cache calling the +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::setMetadataCache` +method of the Validator builder:: + + use Symfony\Component\Validator\Validation; + use Symfony\Component\Validator\Mapping\Cache\ApcCache; + + $validator = Validation::createValidatorBuilder() + // ... add loaders + ->setMetadataCache(new ApcCache('some_apc_prefix')) + ->getValidator(); + +Using a Custom MetadataFactory +------------------------------ + +All the loaders and the cache are passed to an instance of +:class:`Symfony\\Component\\Validator\\Mapping\\Factory\\LazyLoadingMetadataFactory`. +This class is responsible for creating a ``ClassMetadata`` instance from all the +configured resources. + +You can also use a custom metadata factory implementation by creating a class +which implements +:class:`Symfony\\Component\\Validator\\Mapping\\Factory\\MetadataFactoryInterface`. +You can set this custom implementation using +:method:`Symfony\\Component\\Validator\\ValidatorBuilder::setMetadataFactory`:: + + use Acme\Validation\CustomMetadataFactory; + use Symfony\Component\Validator\Validation; + + $validator = Validation::createValidatorBuilder() + ->setMetadataFactory(new CustomMetadataFactory(...)) + ->getValidator(); + +.. caution:: + + Since you are using a custom metadata factory, you can't configure loaders + and caches using the ``add*Mapping()`` methods anymore. You now have to + inject them into your custom metadata factory yourself. + +.. _`Packagist`: https://packagist.org diff --git a/components/var_dumper/introduction.rst b/components/var_dumper.rst similarity index 79% rename from components/var_dumper/introduction.rst rename to components/var_dumper.rst index 2f125192763..5071fa6828e 100644 --- a/components/var_dumper/introduction.rst +++ b/components/var_dumper.rst @@ -15,10 +15,18 @@ The VarDumper Component Installation ------------ -You can install the component in 2 different ways: +.. code-block:: terminal -* :doc:`Install it via Composer ` (``symfony/var-dumper`` on `Packagist`_); -* Use the official Git repository (https://github.com/symfony/var-dumper). + $ composer require symfony/var-dumper + +Alternatively, you can clone the ``_ repository. + +.. include:: /components/require_autoload.rst.inc + +.. note:: + + If using it inside a Symfony application, make sure that the + DebugBundle is enabled in your ``app/AppKernel.php`` file. .. _components-var-dumper-dump: @@ -42,8 +50,9 @@ use instead of e.g. :phpfunction:`var_dump`. By using it, you'll gain: For example:: require __DIR__.'/vendor/autoload.php'; + // create a variable, which could be anything! - $someVar = '...'; + $someVar = ...; dump($someVar); @@ -58,7 +67,7 @@ current PHP SAPI: .. note:: If you want to catch the dump output as a string, please read the - `advanced documentation `_ which contains examples of it. + :doc:`advanced documentation ` which contains examples of it. You'll also learn how to change the format or redirect the output to wherever you want. @@ -70,14 +79,14 @@ current PHP SAPI: #. Run ``composer global require symfony/var-dumper``; #. Add ``auto_prepend_file = ${HOME}/.composer/vendor/autoload.php`` to your ``php.ini`` file; - #. From time to time, run ``composer global update`` to have the latest - bug fixes. + #. From time to time, run ``composer global update symfony/var-dumper`` + to have the latest bug fixes. DebugBundle and Twig Integration -------------------------------- -The ``DebugBundle`` allows greater integration of the component into the -Symfony full stack framework. It is enabled by default in the *dev* and *test* +The DebugBundle allows greater integration of the component into the Symfony +full-stack framework. It is enabled by default in the *dev* and *test* environment of the standard edition since version 2.6. Since generating (even debug) output in the controller or in the model @@ -85,7 +94,7 @@ of your application may just break it by e.g. sending HTTP headers or corrupting your view, the bundle configures the ``dump()`` function so that variables are dumped in the web debug toolbar. -But if the toolbar can not be displayed because you e.g. called ``die``/``exit`` +But if the toolbar cannot be displayed because you e.g. called ``die``/``exit`` or a fatal error occurred, then dumps are written on the regular output. In a Twig template, two constructs are available for dumping a variable. @@ -98,29 +107,9 @@ Choosing between both is mostly a matter of personal taste, still: be suited to your use case (e.g. you shouldn't use it in an HTML attribute or a ``'; + $urlValidator = new Constraints\UrlValidator(); + $urlConstraint = new Constraints\Url(); + + // The URL is wrong, so var_dump() should display an error, but it displays + // "null" instead because there is no context to build a validator violation + var_dump($urlValidator->validate($wrongUrl, $urlConstraint)); + +Reproducing Complex Bugs +------------------------ + +If the bug is related to the Symfony Framework or if it's too complex to create +a PHP script, it's better to reproduce the bug by forking the Symfony Standard +edition. To do so: + +#. Go to https://github.com/symfony/symfony-standard and click on the **Fork** + button to make a fork of that repository or go to your already forked copy. +#. Clone the forked repository into your computer: + ``git clone git://github.com/YOUR-GITHUB-USERNAME/symfony-standard.git`` +#. Browse the project and create a new branch (e.g. ``issue_23567``, + ``reproduce_23657``, etc.) +#. Add and commit the changes generated by Symfony. +#. Now you must add the minimum amount of code to reproduce the bug. This is the + trickiest part and it's explained a bit more later. +#. Add, commit and push all your own changes. +#. Add a comment in your original issue report to share the URL of your forked + project (e.g. ``https://github.com/YOUR-GITHUB-USERNAME/symfony-standard/tree/issue_23567``) + and, if necessary, explain the steps to reproduce (e.g. "browse this URL", + "fill in this data in the form and submit it", etc.) + +Adding the Minimum Amount of Code Possible +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The key to create a bug reproducer is to solely focus on the feature that you +suspect is failing. For example, imagine that you suspect that the bug is related +to a route definition. Then, after forking the Symfony Standard Edition: + +#. Don't edit any of the default Symfony configuration options. +#. Don't copy your original application code and don't use the same structure + of bundles, controllers, actions, etc. as in your original application. +#. Open the default controller class of the AppBundle and add your routing + definition using annotations. +#. Don't create or modify any other file. +#. Execute the ``server:run`` command and browse the previously defined route + to see if the bug appears or not. +#. If you can see the bug, you're done and you can already share the code with us. +#. If you can't see the bug, you must keep making small changes. For example, if + your original route was defined using XML, forget about the previous route + annotation and define the route using XML instead. Or maybe your application + uses bundle inheritance and that's where the real bug is. Then, forget about + AppBundle and quickly generate a new AppParentBundle, make AppBundle inherit + from it and test if the route is working. + +In short, the idea is to keep adding small and incremental changes to the default +Symfony Standard edition until you can reproduce the bug. diff --git a/contributing/code/security.rst b/contributing/code/security.rst index a9260491486..e1c95f4f171 100644 --- a/contributing/code/security.rst +++ b/contributing/code/security.rst @@ -11,17 +11,17 @@ Reporting a Security Issue If you think that you have found a security issue in Symfony, don't use the bug tracker and don't publish it publicly. Instead, all security issues must be sent to **security [at] symfony.com**. Emails sent to this address are -forwarded to the Symfony core-team private mailing-list. +forwarded to the Symfony core team private mailing-list. Resolving Process ----------------- For each report, we first try to confirm the vulnerability. When it is -confirmed, the core-team works on a solution following these steps: +confirmed, the core team works on a solution following these steps: #. Send an acknowledgement to the reporter; #. Work on a patch; -#. Get a CVE identifier from mitre.org; +#. Get a CVE identifier from `mitre.org`_; #. Write a security announcement for the official Symfony `blog`_ about the vulnerability. This post should contain the following information: @@ -37,7 +37,6 @@ confirmed, the core-team works on a solution following these steps: #. Package new versions for all affected versions; #. Publish the post on the official Symfony `blog`_ (it must also be added to the "`Security Advisories`_" category); -#. Update the security advisory list (see below). #. Update the public `security advisories database`_ maintained by the FriendsOfPHP organization and which is used by the ``security:check`` command. @@ -98,42 +97,15 @@ Security Advisories .. tip:: You can check your Symfony application for known security vulnerabilities - using the ``security:check`` command. See :ref:`book-security-checking-vulnerabilities`. - -This section indexes security vulnerabilities that were fixed in Symfony -releases, starting from Symfony 1.0.0: - -* May 26, 2015: `CVE-2015-4050: ESI unauthorized access `_ (Symfony 2.3.29, 2.5.12 and 2.6.8) -* April 1, 2015: `CVE-2015-2309: Unsafe methods in the Request class `_ (Symfony 2.3.27, 2.5.11 and 2.6.6) -* April 1, 2015: `CVE-2015-2308: Esi Code Injection `_ (Symfony 2.3.27, 2.5.11 and 2.6.6) -* September 3, 2014: `CVE-2014-6072: CSRF vulnerability in the Web Profiler `_ (Symfony 2.3.19, 2.4.9 and 2.5.4) -* September 3, 2014: `CVE-2014-6061: Security issue when parsing the Authorization header `_ (Symfony 2.3.19, 2.4.9 and 2.5.4) -* September 3, 2014: `CVE-2014-5245: Direct access of ESI URLs behind a trusted proxy `_ (Symfony 2.3.19, 2.4.9 and 2.5.4) -* September 3, 2014: `CVE-2014-5244: Denial of service with a malicious HTTP Host header `_ (Symfony 2.3.19, 2.4.9 and 2.5.4) -* July 15, 2014: `Security releases: Symfony 2.3.18, 2.4.8, and 2.5.2 released `_ (`CVE-2014-4931 `_) -* October 10, 2013: `Security releases: Symfony 2.0.25, 2.1.13, 2.2.9, and 2.3.6 released `_ (`CVE-2013-5958 `_) -* August 7, 2013: `Security releases: Symfony 2.0.24, 2.1.12, 2.2.5, and 2.3.3 released `_ (`CVE-2013-4751 `_ and `CVE-2013-4752 `_) -* January 17, 2013: `Security release: Symfony 2.0.22 and 2.1.7 released `_ (`CVE-2013-1348 `_ and `CVE-2013-1397 `_) -* December 20, 2012: `Security release: Symfony 2.0.20 and 2.1.5 `_ (`CVE-2012-6431 `_ and `CVE-2012-6432 `_) -* November 29, 2012: `Security release: Symfony 2.0.19 and 2.1.4 `_ -* November 25, 2012: `Security release: symfony 1.4.20 released `_ (`CVE-2012-5574 `_) -* August 28, 2012: `Security Release: Symfony 2.0.17 released `_ -* May 30, 2012: `Security Release: symfony 1.4.18 released `_ (`CVE-2012-2667 `_) -* February 24, 2012: `Security Release: Symfony 2.0.11 released `_ -* November 16, 2011: `Security Release: Symfony 2.0.6 `_ -* March 21, 2011: `symfony 1.3.10 and 1.4.10: security releases `_ -* June 29, 2010: `Security Release: symfony 1.3.6 and 1.4.6 `_ -* May 31, 2010: `symfony 1.3.5 and 1.4.5 `_ -* February 25, 2010: `Security Release: 1.2.12, 1.3.3 and 1.4.3 `_ -* February 13, 2010: `symfony 1.3.2 and 1.4.2 `_ -* April 27, 2009: `symfony 1.2.6: Security fix `_ -* October 03, 2008: `symfony 1.1.4 released: Security fix `_ -* May 14, 2008: `symfony 1.0.16 is out `_ -* April 01, 2008: `symfony 1.0.13 is out `_ -* March 21, 2008: `symfony 1.0.12 is (finally) out ! `_ -* June 25, 2007: `symfony 1.0.5 released (security fix) `_ + using the ``security:check`` command (see :doc:`/security/security_checker`). + +Check the `Security Advisories`_ blog category for a list of all security +vulnerabilities that were fixed in Symfony releases, starting from Symfony +1.0.0. .. _Git repository: https://github.com/symfony/symfony .. _blog: https://symfony.com/blog/ .. _Security Advisories: https://symfony.com/blog/category/security-advisories .. _`security advisories database`: https://github.com/FriendsOfPHP/security-advisories +.. _`mitre.org`: https://cveform.mitre.org/ +.. _`Security Advisories`: https://symfony.com/blog/category/security-advisories diff --git a/contributing/code/standards.rst b/contributing/code/standards.rst index 2168129d7fa..c62192c2ccf 100644 --- a/contributing/code/standards.rst +++ b/contributing/code/standards.rst @@ -1,19 +1,33 @@ Coding Standards ================ -When contributing code to Symfony, you must follow its coding standards. To -make a long story short, here is the golden rule: **Imitate the existing -Symfony code**. Most open-source Bundles and libraries used by Symfony also -follow the same guidelines, and you should too. +Symfony code is contributed by thousands of developers around the world. To make +every piece of code look and feel familiar, Symfony defines some coding standards +that all contributions must follow. -Remember that the main advantage of standards is that every piece of code -looks and feels familiar, it's not about this or that being more readable. +These Symfony coding standards are based on the `PSR-1`_, `PSR-2`_ and `PSR-4`_ +standards, so you may already know most of them. -Symfony follows the standards defined in the `PSR-0`_, `PSR-1`_, `PSR-2`_ and `PSR-4`_ -documents. +Making your Code Follow the Coding Standards +-------------------------------------------- -Since a picture - or some code - is worth a thousand words, here's a short -example containing most features described below: +Instead of reviewing your code manually, Symfony makes it simple to ensure that +your contributed code matches the expected code syntax. First, install the +`PHP CS Fixer tool`_ and then, run this command to fix any problem: + +.. code-block:: terminal + + $ cd your-project/ + $ php php-cs-fixer.phar fix -v + +If you forget to run this command and make a pull request with any syntax issue, +our automated tools will warn you about that and will provide the solution. + +Symfony Coding Standards in Detail +---------------------------------- + +If you want to learn about the Symfony coding standards in detail, here's a +short example containing most features described below: .. code-block:: html+php @@ -37,6 +51,9 @@ example containing most features described below: { const SOME_CONST = 42; + /** + * @var string + */ private $fooBar; /** @@ -48,25 +65,47 @@ example containing most features described below: } /** - * @param string $dummy Some argument description - * @param array $options + * @return string * - * @return string|null Transformed input + * @deprecated + */ + public function someDeprecatedMethod() + { + @trigger_error(sprintf('The %s() method is deprecated since version 2.8 and will be removed in 3.0. Use Acme\Baz::someMethod() instead.', __METHOD__), E_USER_DEPRECATED); + + return Baz::someMethod(); + } + + /** + * Transforms the input given as first argument. + * + * @param bool|string $dummy Some argument description + * @param array $options An options collection to be used within the transformation * - * @throws \RuntimeException + * @return string|null The transformed input + * + * @throws \RuntimeException When an invalid option is provided */ private function transformText($dummy, array $options = array()) { + $defaultOptions = array( + 'some_default' => 'values', + 'another_default' => 'more values', + ); + + foreach ($options as $option) { + if (!in_array($option, $defaultOptions)) { + throw new \RuntimeException(sprintf('Unrecognized option "%s"', $option)); + } + } + $mergedOptions = array_merge( - array( - 'some_default' => 'values', - 'another_default' => 'more values', - ), + $defaultOptions, $options ); if (true === $dummy) { - return; + return null; } if ('string' === $dummy) { @@ -76,10 +115,16 @@ example containing most features described below: return ucwords($dummy); } - - throw new \RuntimeException(sprintf('Unrecognized dummy option "%s"', $dummy)); } + /** + * Performs some basic check for a given value. + * + * @param mixed $value Some value to check against + * @param bool $theSwitch Some switch to control the method's flow + * + * @return bool|void The resultant check if $theSwitch isn't false, void otherwise + */ private function reverseBoolean($value = null, $theSwitch = false) { if (!$theSwitch) { @@ -91,7 +136,7 @@ example containing most features described below: } Structure ---------- +~~~~~~~~~ * Add a single space after each comma delimiter; @@ -112,6 +157,9 @@ Structure * Add a blank line before ``return`` statements, unless the return is alone inside a statement-group (like an ``if`` statement); +* Use ``return null;`` when a function explicitly returns ``null`` values and + use ``return;`` when the function returns ``void`` values; + * Use braces to indicate control structure body regardless of the number of statements it contains; @@ -119,31 +167,49 @@ Structure that are not intended to be instantiated from the outside and thus are not concerned by the `PSR-0`_ and `PSR-4`_ autoload standards; +* Declare the class inheritance and all the implemented interfaces on the same + line as the class name; + * Declare class properties before methods; * Declare public methods first, then protected ones and finally private ones. - The exceptions to this rule are the class constructor and the ``setUp`` and - ``tearDown`` methods of PHPUnit tests, which should always be the first methods + The exceptions to this rule are the class constructor and the ``setUp()`` and + ``tearDown()`` methods of PHPUnit tests, which must always be the first methods to increase readability; +* Declare all the arguments on the same line as the method/function name, no + matter how many arguments there are; + * Use parentheses when instantiating classes regardless of the number of arguments the constructor has; -* Exception message strings should be concatenated using :phpfunction:`sprintf`. +* Exception and error message strings must be concatenated using :phpfunction:`sprintf`; + +* Calls to :phpfunction:`trigger_error` with type ``E_USER_DEPRECATED`` must be + switched to opt-in via ``@`` operator. + Read more at :ref:`contributing-code-conventions-deprecations`; + +* Do not use ``else``, ``elseif``, ``break`` after ``if`` and ``case`` conditions + which return or throw something; + +* Do not use spaces around ``[`` offset accessor and before ``]`` offset accessor; + +* Add a ``use`` statement for every class that is not part of the global namespace. Naming Conventions ------------------- +~~~~~~~~~~~~~~~~~~ * Use camelCase, not underscores, for variable, function and method names, arguments; -* Use underscores for option names and parameter names; +* Use underscores for configuration options and parameters; * Use namespaces for all classes; -* Prefix abstract classes with ``Abstract``. Please note some early Symfony classes - do not follow this convention and have not been renamed for backward compatibility - reasons. However all new abstract classes must follow this naming convention; +* Prefix all abstract classes with ``Abstract`` except PHPUnit ``*TestCase``. + Please note some early Symfony classes do not follow this convention and + have not been renamed for backward compatibility reasons. However all new + abstract classes must follow this naming convention; * Suffix interfaces with ``Interface``; @@ -174,23 +240,37 @@ Service Naming Conventions * A group name uses the underscore notation. Documentation -------------- +~~~~~~~~~~~~~ + +* Add PHPDoc blocks for all classes, methods, and functions (though you may + be asked to remove PHPDoc that do not add value); -* Add PHPDoc blocks for all classes, methods, and functions; +* Group annotations together so that annotations of the same type immediately + follow each other, and annotations of a different type are separated by a + single blank line; * Omit the ``@return`` tag if the method does not return anything; -* The ``@package`` and ``@subpackage`` annotations are not used. +* The ``@package`` and ``@subpackage`` annotations are not used; + +* Don't inline PHPDoc blocks, even when they contain just one tag (e.g. don't + put ``/** {@inheritdoc} */`` in a single line); + +* When adding a new class or when making significant changes to an existing class, + an ``@author`` tag with personal contact information may be added, or expanded. + Please note it is possible to have the personal contact information updated or + removed per request to the doc:`core team `. License -------- +~~~~~~~ * Symfony is released under the MIT license, and the license block has to be present at the top of every PHP file, before the namespace. -.. _`PSR-0`: http://www.php-fig.org/psr/psr-0/ -.. _`PSR-1`: http://www.php-fig.org/psr/psr-1/ -.. _`PSR-2`: http://www.php-fig.org/psr/psr-2/ -.. _`PSR-4`: http://www.php-fig.org/psr/psr-4/ +.. _`PHP CS Fixer tool`: http://cs.sensiolabs.org/ +.. _`PSR-0`: https://www.php-fig.org/psr/psr-0/ +.. _`PSR-1`: https://www.php-fig.org/psr/psr-1/ +.. _`PSR-2`: https://www.php-fig.org/psr/psr-2/ +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ .. _`identical comparison`: https://php.net/manual/en/language.operators.comparison.php .. _`Yoda conditions`: https://en.wikipedia.org/wiki/Yoda_conditions diff --git a/contributing/code/tests.rst b/contributing/code/tests.rst index 6e14a9d412f..968ad1c95cf 100644 --- a/contributing/code/tests.rst +++ b/contributing/code/tests.rst @@ -3,96 +3,58 @@ Running Symfony Tests ===================== -Before submitting a :doc:`patch ` for inclusion, you need to run the -Symfony test suite to check that you have not broken anything. +The Symfony project uses a third-party service which automatically runs tests +for any submitted :doc:`patch `. If the new code breaks any test, +the pull request will show an error message with a link to the full error details. -PHPUnit -------- +In any case, it's a good practice to run tests locally before submitting a +:doc:`patch ` for inclusion, to check that you have not broken anything. -To run the Symfony test suite, `install PHPUnit`_ 4.2 (or later) first. +.. _phpunit: +.. _dependencies_optional: -Dependencies (optional) ------------------------ +Before Running the Tests +------------------------ -To run the entire test suite, including tests that depend on external -dependencies, Symfony needs to be able to autoload them. By default, they are -autoloaded from ``vendor/`` under the main root directory (see -``autoload.php.dist``). +To run the Symfony test suite, install the external dependencies used during the +tests, such as Doctrine, Twig and Monolog. To do so, +:doc:`install Composer ` and execute the following: -The test suite needs the following third-party libraries: - -* Doctrine -* Swift Mailer -* Twig -* Monolog - -To install them all, use `Composer`_: - -Step 1: :doc:`Install Composer globally ` - -Step 2: Install vendors. - -.. code-block:: bash - - $ composer install - -.. note:: - - Note that the script takes some time to finish. - -After installation, you can update the vendors to their latest version with -the follow command: - -.. code-block:: bash +.. code-block:: terminal $ composer update -Running -------- +.. _running: -First, update the vendors (see above). +Running the Tests +----------------- Then, run the test suite from the Symfony root directory with the following command: -.. code-block:: bash +.. code-block:: terminal - $ phpunit + $ php ./phpunit symfony -The output should display ``OK``. If not, you need to figure out what's going on -and if the tests are broken because of your modifications. +The output should display ``OK``. If not, read the reported errors to figure out +what's going on and if the tests are broken because of the new code. .. tip:: - If you want to test a single component type its path after the ``phpunit`` - command, e.g.: - - .. code-block:: bash - - $ phpunit src/Symfony/Component/Finder/ - -.. tip:: - - Run the test suite before applying your modifications to check that they - run fine on your configuration. - -Code Coverage -------------- - -If you add a new feature, you also need to check the code coverage by using -the ``coverage-html`` option: - -.. code-block:: bash + The entire Symfony suite can take up to several minutes to complete. If you + want to test a single component, type its path after the ``phpunit`` command, + e.g.: - $ phpunit --coverage-html=cov/ + .. code-block:: terminal -Check the code coverage by opening the generated ``cov/index.html`` page in a -browser. + $ php ./phpunit src/Symfony/Component/Finder/ .. tip:: - The code coverage only works if you have Xdebug enabled and all - dependencies installed. + On Windows, install the `Cmder`_, `ConEmu`_, `ANSICON`_ or `Mintty`_ free applications + to see colored test results. -.. _install PHPUnit: https://phpunit.de/manual/current/en/installation.html -.. _`Composer`: https://getcomposer.org/ +.. _Cmder: http://cmder.net/ +.. _ConEmu: https://conemu.github.io/ +.. _ANSICON: https://github.com/adoxa/ansicon/releases +.. _Mintty: https://mintty.github.io/ diff --git a/contributing/code_of_conduct/care_team.rst b/contributing/code_of_conduct/care_team.rst new file mode 100644 index 00000000000..0829dfef45e --- /dev/null +++ b/contributing/code_of_conduct/care_team.rst @@ -0,0 +1,48 @@ +CARE Team +========= + +Our Pledge +---------- + +In the interest of fostering an open and welcoming environment, the CoC Active Response Ensurers, or CARE, +pledge to ensure that the spirit of the :doc:`Code of Conduct ` +is respected. Our main priority is to ensure the safety of our community members. +The second goal is to help educate the community as a whole to be aware of the CoC +and how to help implement its spirit throughout the community. In case these goals +conflict, we will prioritize safety of community members over all other goals. + +If you think there is or has been a violation to the code of conduct please contact +the CARE team or if you prefer contact only individual members of the CARE team. + +Members +------- + +Here are all the members of the Code of Conduct CARE team (in alphabetic order). +You can contact any of them directly using the contact details below or you can +also contact all of them at once by emailing **coc@symfony.com**: + +* **Emilie Lorenzo** + + * *E-mail*: emilie.lorenzo [at] symfony.com + * *Twitter*: `@EmilieLorenzo `_ + * *SensioConnect*: `emilielorenzo `_ + +* **Tobias Nyholm** + + * *E-mail*: tobias.nyholm [at] gmail.com + * *Twitter*: `@tobiasnyholm `_ + * *SensioConnect*: `tobias `_ + +* **Michelle Sanver** + + * *E-mail*: hello [at] michellesanver.com + * *Twitter*: `@michellesanver `_ + * *SensioConnect*: `michellesanver `_ + +About the CARE Team +------------------- + +The :doc:`Symfony project leader ` appoints the CARE +team with candidates they see fit. The CARE team will consist of at least +3 people. The team should be representing as many demographics as possible, +ideally from different employers. diff --git a/contributing/code_of_conduct/code_of_conduct.rst b/contributing/code_of_conduct/code_of_conduct.rst new file mode 100644 index 00000000000..7ecd91e8dbd --- /dev/null +++ b/contributing/code_of_conduct/code_of_conduct.rst @@ -0,0 +1,92 @@ +Code of Conduct +=============== + +Our Pledge +---------- + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnic origin, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +religion, or sexual identity and orientation. + +Our Standards +------------- + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +Our Responsibilities +-------------------- + +:doc:`CoC Active Response Ensurers, or CARE`, +are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +CARE team members have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +Scope +----- + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by CARE team members. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior +:doc:`may be reported ` +by contacting the :doc:`CARE team members `. +All complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The CARE team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +CARE team members who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by the +:doc:`core team `. + +Attribution +----------- + +This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +Related Documents +----------------- + +.. toctree:: + :maxdepth: 1 + + reporting_guidelines + care_team + concrete_example_document + +.. _Contributor Covenant: https://www.contributor-covenant.org diff --git a/contributing/code_of_conduct/concrete_example_document.rst b/contributing/code_of_conduct/concrete_example_document.rst new file mode 100644 index 00000000000..6f1277a625a --- /dev/null +++ b/contributing/code_of_conduct/concrete_example_document.rst @@ -0,0 +1,30 @@ +Code of Conduct: Concrete Example Document +========================================== + +This is a living document that serves to give concrete examples of +unwanted behaviour. These examples have all taken place somewhere in the +PHP community in the past, and are clear code of conduct violations +according to the Symfony code of conduct. + +Concrete Examples +----------------- + +* Unwelcome comments regarding a person’s lifestyle choices and practices, + including those related to food, health, parenting, drugs, and employment; +* Deliberate misgendering or use of `dead names`_ (The birth name + of a person who has since changed their name, often a transgender person); +* Threats of violence like "The person that created this PR should be + punched in the face"; +* Incitement of violence towards any individual, including encouraging a + person to commit suicide or to engage in self-harm (even as a joke); +* Sustained disruption of discussion; +* Pattern of inappropriate social contact, such as requesting/assuming + inappropriate levels of intimacy with others; +* Continued one-on-one communication after requests to cease; +* Putting down people based on their technology choices or their work. + +The original list is inspired and modified from `geek feminism`_ and +confirmed by experiences from PHPWomen. + +.. _dead names: https://en.wiktionary.org/wiki/deadname +.. _geek feminism: https://geekfeminism.org/about/code-of-conduct diff --git a/contributing/code_of_conduct/index.rst b/contributing/code_of_conduct/index.rst new file mode 100644 index 00000000000..5a2beff23a9 --- /dev/null +++ b/contributing/code_of_conduct/index.rst @@ -0,0 +1,10 @@ +Code of Conduct +=============== + +.. toctree:: + :maxdepth: 2 + + code_of_conduct + reporting_guidelines + care_team + concrete_example_document diff --git a/contributing/code_of_conduct/reporting_guidelines.rst b/contributing/code_of_conduct/reporting_guidelines.rst new file mode 100644 index 00000000000..1766d025e4d --- /dev/null +++ b/contributing/code_of_conduct/reporting_guidelines.rst @@ -0,0 +1,98 @@ +Reporting Guidelines +==================== + +If you believe someone is violating the Code of Conduct we ask that you report +it to the :doc:`CARE team ` +by emailing, Twitter, in person or any way you see fit. + +**All reports will be kept confidential.** The privacy of everyone included in +the report is of our highest concern. Second to privacy there is transparency. +After every report we will determine if a public statement should be made. If +that's the case, the identities of all victims, reporters, and the accused will +remain confidential unless those individuals instruct us otherwise. The details +of the incident may also be generalized. + +If you believe anyone is in physical danger or doing something that is against +the law, please notify appropriate emergency services first by calling the relevant +local authorities. If you are unsure what service or agency is appropriate to +contact, include this in your report and we will attempt to notify them. + +In your report please include: + +* Your contact info for follow-up contact. +* Names (legal, nicknames, or pseudonyms) of any individuals involved. +* If there were other witnesses besides you, please try to include them as well. +* When and where the incident occurred. Please be as specific as possible. +* Your description of what occurred. +* If there is a publicly available record (e.g. a mailing list archive or a + public IRC or Slack log), please include a link and a screenshot. +* If you believe this incident is ongoing. +* Any other information you believe we should have. + +What happens after you file a report? +------------------------------------- + +You will receive a reply from the :doc:`CARE team ` +acknowledging receipt as soon as possible, but within 24 hours. + +The team member receiving the report will immediately contact all or some other +CARE team members to review the incident and determine: + +* What happened. +* Whether this event constitutes a Code of Conduct violation. +* What kind of response is appropriate. + +If this is determined to be an ongoing incident or a threat to physical safety, +the team's immediate priority will be to protect everyone involved. This means +we may delay an "official" response until we believe that the situation has ended +and that everyone is physically safe. + +Once the team has a complete account of the events, they will make a decision as +to how to respond. Responses may include: + +* Nothing (if we determine no Code of Conduct violation occurred). +* A private reprimand from the Code of Conduct response team to the individual(s) + involved. +* An imposed vacation (i.e. asking someone to "take a week off" from a mailing + list or Slack). +* A permanent or temporary ban from some or all Symfony conference/community + spaces (events, meetings, mailing lists, IRC, Slack, etc.) +* A request to engage in mediation and/or an accountability plan. +* On a case by case basis, other actions may be possible but will usually be + coordinated with the core team and the Symfony company. + +We'll respond within one week to the person who filed the report with either a +resolution or an explanation of why the situation is not yet resolved. + +Once we've determined our final actions, we'll contact the original reporter to +let them know what action (if any) we'll be taking. We'll take into account feedback +from the reporter on the appropriateness of our response, but our response will be +determined by what will be best for community safety. + +The CARE team keeps a private record of all incidents. By default all reports +are shared with the entire CARE team unless the reporter specifically asks +to exclude specific CARE team members, in which case these CARE team +members will not be included in any communication on the incidents as well as records +created related to the incidents. + +CARE team members are expected to inform the CARE team and the reporters +in case of conflicts on interest and recuse themselves if this is deemed a problem. + +Appealing the response +---------------------- + +Only permanent resolutions (such as bans) may be appealed. To appeal a decision +of the working group, contact the :doc:`CARE team ` +with your appeal and they will review the case. + +Document origin +--------------- + +Reporting Guidelines derived from those of the `Stumptown Syndicate`_ and the +`Django Software Foundation`_. + +Adopted by `Symfony`_ organizers on 21 February 2018. + +.. _`Stumptown Syndicate`: http://stumptownsyndicate.org/code-of-conduct/reporting-guidelines/ +.. _`Django Software Foundation`: https://www.djangoproject.com/conduct/reporting/ +.. _`Symfony`: https://symfony.com diff --git a/contributing/community/index.rst b/contributing/community/index.rst index 43b7b527ee3..391f0d585c3 100644 --- a/contributing/community/index.rst +++ b/contributing/community/index.rst @@ -5,4 +5,6 @@ Community :maxdepth: 2 releases + review-comments + reviews other diff --git a/contributing/community/other.rst b/contributing/community/other.rst index 3869aa2a4e0..2196ccb925c 100644 --- a/contributing/community/other.rst +++ b/contributing/community/other.rst @@ -12,4 +12,4 @@ these additional resources: .. _pull requests: https://github.com/symfony/symfony/pulls .. _commits: https://github.com/symfony/symfony/commits/master .. _bugs and enhancements: https://github.com/symfony/symfony/issues -.. _bundles: http://knpbundles.com/ +.. _bundles: https://github.com/search?q=topic%3Asymfony-bundle&type=Repositories diff --git a/contributing/community/releases.rst b/contributing/community/releases.rst index 3e46cbcb667..a7dbd76aadc 100644 --- a/contributing/community/releases.rst +++ b/contributing/community/releases.rst @@ -1,36 +1,30 @@ The Release Process =================== -This document explains the Symfony release process (Symfony being the code -hosted on the main ``symfony/symfony`` `Git repository`_). +This document explains the **release process** of the Symfony project (i.e. the +code hosted on the main ``symfony/symfony`` `Git repository`_). -Symfony manages its releases through a *time-based model*; a new Symfony minor -version comes out every *six months*: one in *May* and one in *November*. +Symfony manages its releases through a *time-based model* and follows the +`Semantic Versioning`_ strategy: -.. tip:: - - The meaning of "minor" comes from the `Semantic Versioning`_ strategy. - -Each minor version sticks to the same very well-defined process where we start -with a development period, followed by a maintenance period. - -.. note:: - - This release process has been adopted as of Symfony 2.2, and all the - "rules" explained in this document must be strictly followed as of Symfony - 2.4. +* A new Symfony minor version (e.g. 2.8, 3.2, 4.1) comes out every *six months*: + one in *May* and one in *November*; +* A new Symfony major version (e.g., 3.0, 4.0) comes out every *two years* and + it's released at the same time of the last minor version of the previous major + version. .. _contributing-release-development: Development ----------- -The full development period lasts six months and is divided into two phases: +The full development period for any major or minor version lasts six months and +is divided into two phases: -* *Development*: *Four months* to add new features and to enhance existing +* **Development**: *Four months* to add new features and to enhance existing ones; -* *Stabilisation*: *Two months* to fix bugs, prepare the release, and wait +* **Stabilization**: *Two months* to fix bugs, prepare the release, and wait for the whole Symfony ecosystem (third-party libraries, bundles, and projects using Symfony) to catch up. @@ -43,30 +37,42 @@ final release. Maintenance ----------- -Each Symfony minor version is maintained for a fixed period of time, depending -on the type of the release. We have two maintenance periods: +Each Symfony version is maintained for a fixed period of time, depending on the +type of the release. This maintenance is divided into: * *Bug fixes and security fixes*: During this period, all issues can be fixed. The end of this period is referenced as being the *end of maintenance* of a release. * *Security fixes only*: During this period, only security related issues can - be fixed. The end of this period is referenced as being the *end of - life* of a release. + be fixed. The end of this period is referenced as being the *end of life* of + a release. + +.. note:: + + The :doc:`maintenance document ` describes + the boundaries of acceptable changes during maintenance. + +Symfony Versions +---------------- Standard Versions ~~~~~~~~~~~~~~~~~ -A standard minor version is maintained for an *eight month* period for bug +A **Standard Minor Version** is maintained for an *eight month* period for bug fixes, and for a *fourteen month* period for security issue fixes. +In Symfony 2.x branch, the number of minor versions wasn't constrained, so that +branch ended up with nine minor versions (from 2.0 to 2.8). Starting from +3.x branch, the number of minor versions is limited to five (from X.0 to X.4). + .. _releases-lts: Long Term Support Versions ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Every two years, a new Long Term Support Version (aka LTS version) is -published. Each LTS version is supported for a *three year* period for bug +Every two years, a new **Long Term Support Version** (usually abbreviated as "LTS") +is published. Each LTS version is supported for a *three year* period for bug fixes, and for a *four year* period for security issue fixes. .. note:: @@ -74,47 +80,26 @@ fixes, and for a *four year* period for security issue fixes. Paid support after the three year support provided by the community can also be bought from `SensioLabs`_. +In the Symfony 2.x branch, the LTS versions are 2.3, 2.7 and 2.8. Starting from the 3.x +branch, only the last minor version of each branch is considered LTS (e.g. 3.4, +4.4, 5.4, etc.) + Schedule -------- Below is the schedule for the first few versions that use this release model: -.. image:: /images/contributing/release-process.jpg +.. image:: /_images/contributing/release-process.jpg :align: center * **Yellow** represents the Development phase -* **Blue** represents the Stabilisation phase +* **Blue** represents the Stabilization phase * **Green** represents the Maintenance period -This results in very predictable dates and maintenance periods: - -======= ============== ======= ======================== =========== -Version Feature Freeze Release End of Maintenance End of Life -======= ============== ======= ======================== =========== -2.0 05/2011 07/2011 03/2013 (20 months) 09/2013 -2.1 07/2012 09/2012 05/2013 (9 months) 11/2013 -2.2 01/2013 03/2013 11/2013 (8 months) 05/2014 -**2.3** 03/2013 05/2013 05/2016 (36 months) 05/2017 -2.4 09/2013 11/2013 09/2014 (10 months [1]_) 01/2015 -2.5 03/2014 05/2014 01/2015 (8 months) 07/2015 -2.6 09/2014 11/2014 07/2015 (8 months) 01/2016 -**2.7** 03/2015 05/2015 05/2018 (36 months) 05/2019 -**2.8** 09/2015 11/2015 11/2018 (36 months [2]_) 11/2019 -3.0 09/2015 11/2015 07/2016 (8 months) 01/2017 -3.1 03/2016 05/2016 01/2017 (8 months) 07/2017 -3.2 09/2016 11/2016 07/2017 (8 months) 01/2018 -**3.3** 03/2017 05/2017 05/2020 (36 months) 05/2021 -... ... ... ... ... -======= ============== ======= ======================== =========== - -.. [1] Symfony 2.4 maintenance has been `extended to September 2014`_. -.. [2] Symfony 2.8 is the last version of the Symfony 2.x branch. - .. tip:: If you want to learn more about the timeline of any given Symfony version, - use the online `timeline calculator`_. You can also get all data as a JSON - string via a URL like `https://symfony.com/roadmap.json?version=2.x`. + use the online `timeline calculator`_. .. tip:: @@ -123,21 +108,48 @@ Version Feature Freeze Release End of Maintenance End of Life instance), you can automatically receive an email notification if you subscribed on the `roadmap notification`_ page. -Backwards Compatibility ------------------------ +.. _version-history: + +============ ============== ======= ======================== =========== +Version Feature Freeze Release End of Maintenance End of Life +============ ============== ======= ======================== =========== +`2.0`_ 05/2011 07/2011 03/2013 (20 months) 09/2013 +`2.1`_ 07/2012 09/2012 05/2013 (9 months) 11/2013 +`2.2`_ 01/2013 03/2013 11/2013 (8 months) 05/2014 +`2.3`_ (LTS) 03/2013 05/2013 05/2016 (36 months) 05/2017 +`2.4`_ 09/2013 11/2013 09/2014 (10 months [1]_) 01/2015 +`2.5`_ 03/2014 05/2014 01/2015 (8 months) 07/2015 +`2.6`_ 09/2014 11/2014 07/2015 (8 months) 01/2016 +`2.7`_ (LTS) 03/2015 05/2015 05/2018 (36 months) 05/2019 +`2.8`_ (LTS) 09/2015 11/2015 11/2018 (36 months [2]_) 11/2019 +`3.0`_ 09/2015 11/2015 07/2016 (8 months) [3]_) 01/2017 +`3.1`_ 03/2016 05/2016 01/2017 (8 months) 07/2017 +`3.2`_ 09/2016 11/2016 07/2017 (8 months) 01/2018 +`3.3`_ 03/2017 05/2017 01/2018 (8 months) 07/2018 +`3.4`_ (LTS) 09/2017 11/2017 11/2020 (36 months) 11/2021 +`4.0`_ 09/2017 11/2017 07/2018 (8 months) 01/2019 +`4.1`_ 03/2018 05/2018 01/2019 (8 months) 07/2019 +`4.2`_ 09/2018 11/2018 07/2019 (8 months) 01/2020 +`4.3`_ 03/2019 05/2019 01/2020 (8 months) 07/2020 +`4.4`_ (LTS) 09/2019 11/2019 11/2022 (36 months) 11/2023 +`5.0`_ 09/2019 11/2019 07/2020 (8 months) 01/2021 +... ... ... ... ... +============ ============== ======= ======================== =========== -Our :doc:`Backwards Compatibility Promise ` is very +.. [1] Symfony 2.4 maintenance has been `extended to September 2014`_. +.. [2] Symfony 2.8 is the last version of the Symfony 2.x branch. +.. [3] Symfony 3.0 is the first version to use the new release process based on five minor releases. + +Backward Compatibility +---------------------- + +Our :doc:`Backward Compatibility Promise ` is very strict and allows developers to upgrade with confidence from one minor version of Symfony to the next one. Whenever keeping backward compatibility is not possible, the feature, the enhancement or the bug fix will be scheduled for the next major version. -.. note:: - - The work on a new major version of Symfony starts whenever enough major - features breaking backward compatibility are waiting on the todo-list. - Deprecations ------------ @@ -175,9 +187,29 @@ version: a new version is published every six months, and there is a two months period to upgrade. Companies wanting more stability use the LTS versions: a new version is published every two years and there is a year to upgrade. -.. _Semantic Versioning: http://semver.org/ +.. _Semantic Versioning: https://semver.org/ .. _Git repository: https://github.com/symfony/symfony .. _SensioLabs: http://sensiolabs.com/ .. _roadmap notification: https://symfony.com/roadmap .. _extended to September 2014: https://symfony.com/blog/extended-maintenance-for-symfony-2-4 -.. _timeline calculator: https://symfony.com/roadmap +.. _timeline calculator: https://symfony.com/roadmap#checker +.. _`2.0`: https://symfony.com/roadmap?version=2.0 +.. _`2.1`: https://symfony.com/roadmap?version=2.1 +.. _`2.2`: https://symfony.com/roadmap?version=2.2 +.. _`2.3`: https://symfony.com/roadmap?version=2.3 +.. _`2.4`: https://symfony.com/roadmap?version=2.4 +.. _`2.5`: https://symfony.com/roadmap?version=2.5 +.. _`2.6`: https://symfony.com/roadmap?version=2.6 +.. _`2.7`: https://symfony.com/roadmap?version=2.7 +.. _`2.8`: https://symfony.com/roadmap?version=2.8 +.. _`3.0`: https://symfony.com/roadmap?version=3.0 +.. _`3.1`: https://symfony.com/roadmap?version=3.1 +.. _`3.2`: https://symfony.com/roadmap?version=3.2 +.. _`3.3`: https://symfony.com/roadmap?version=3.3 +.. _`3.4`: https://symfony.com/roadmap?version=3.4 +.. _`4.0`: https://symfony.com/roadmap?version=4.0 +.. _`4.1`: https://symfony.com/roadmap?version=4.1 +.. _`4.2`: https://symfony.com/roadmap?version=4.2 +.. _`4.3`: https://symfony.com/roadmap?version=4.3 +.. _`4.4`: https://symfony.com/roadmap?version=4.4 +.. _`5.0`: https://symfony.com/roadmap?version=5.0 diff --git a/contributing/community/review-comments.rst b/contributing/community/review-comments.rst new file mode 100644 index 00000000000..a317c12fa27 --- /dev/null +++ b/contributing/community/review-comments.rst @@ -0,0 +1,178 @@ +Respectful Review Comments +========================== + +:doc:`Reviewing issues and pull requests ` +is a great way to get started with contributing to the Symfony community. +Anyone can do it! But before you give a comment, take a step back and think, +is what you are about to say actually what you intend? + +Communicating over the Internet with nothing but text can pose a +big challenge, especially if you remember that the Symfony community +is world-wide and is composed of a wide variety of people with differing +ideas and opinions. + +Not everyone speaks English or is able to use a keyboard. Some might +have dyslexia or similar conditions that affect their writing. + +Not to mention that some might have a bad experience from previous +contributions (to other projects). + +You're not alone in this. This guide will try to help you write +constructive, respectful and helpful reviews and replies. + +.. tip:: + + This guide is not about lecturing you to "conform" or give-up + your ideas and opinions but helping you to better communicate, + prevent possible confusion, and keeping the Symfony community a + welcoming place for everyone. **You are free to disagree with + someone's opinions, just don't be disrespectful.** + +First of, accept that many programming decisions are opinions. +Discuss trade offs, which you prefer, and reach a resolution quickly. +It's not about being right or wrong, but using what works. + +Tone of Voice +------------- + +We don't expect you to be completely formal, or to even write error-free +English. Just remember this: don't swear, and be respectful to others. + +Don't reply in anger or with an aggressive tone. You're angry, we understand +that, but swearing/cursing and name calling doesn't really encourage anyone to +help you. Take a deep breath, count to 10 and try to *clearly* explain what problems +you encounter. + +Inclusive Language +------------------ + +In an effort to be inclusive to a wide group of people, it's recommended to +use personal pronouns that don't suggest a particular gender. Unless someone +has stated their pronouns, use "they", "them" instead of "he", "she", "his", +"hers", "his/hers", "he/she", etc. + +Try to avoid using wording that may be considered excluding, needlessly gendered +(e.g. words that have a male or female base), racially motivated or singles out +a particular group in society. For example, it's recommended to use words like +"folks", "team", "everyone" instead of "guys", "ladies", "yanks", etc. + +Giving Positive Feedback +------------------------ + +While reviewing issues and pull requests you may run into some suggestions +(including patches) that don't reflect your ideas, are not good, or downright wrong. + +Now, when you prepare your comment, consider the amount of work and time the author +has spent on their idea and how your response would make them feel. + +Did you correctly understand their intention? Or are you making assumptions? +Whatever your response, be explicit. Remember people don't always understand your +intentions online. + +Avoid using terms that could be seen as referring to personal traits ("dumb", "stupid"). +Assume everyone is intelligent and well-meaning. + +.. tip:: + + Good questions avoid judgement and avoid assumptions about the author's perspective. + + Maybe you can ask for clarification? Suggest an alternative? + Or provide a simple explanation *why* you disagree with their proposal. + + * ``This looks wrong. Are you sure it's correct?`` (eg. typo/syntax error) + + * ``What do you think of "RequestFactory" instead of RequestCreator?`` + +Even if something *is* really wrong or "a bad idea", stay respectful and +don't get into endless you-are-wrong discussions or "flame wars". + +Don't use hyperbole ("always", "never", "endlessly", "nothing", "worst", "horrible", "terrible"). + +**Don't:** *"I don't like how you wrote this code"* - there is no clear explanation why you +don't like how it's written. + +**Better:** *"I find it hard to read this code as there many nested if statements, can you make it more +readable? By encapsulating some of it's details or maybe adding some comments to explain the overall logic."* - +You explain why you find the code hard to read *and* give some suggestions for improvement. + +If a piece of code is in fact wrong, explain why: + +* ``This code doesn't comply with Symfony's CS rules. Please see [...] for details``. + +* ``Symfony 3 still uses PHP 5 and doesn't allow the usage scalar type-hints.``. + +* ``I think the code is less readable now`` - careful here, be sure explain why you think + the code is less readable, and maybe give some suggestions? + +**Examples of valid reasons to reject:** + + * We tried that in the past (link to the relevant PR) but we needed to revert it for XXX reason. + + * That change would introduce too many merge conflicts when merging up Symfony branches. + In the past we've always rejected changes like this. + + * I profiled this change and it hurts performance significantly (if you don't profile, it's an opinion, so we can ignore) + + * Code doesn't match Symfony's CS rules (e.g. ``use array()`` instead of ``[]``) + + * We only provide integration with very popular projects (e.g. we integrate Bootstrap but not your own CSS framework) + + * This would require adding lots of code and making lots of changes for a feature that doesn't look so important. + That could hurt maintaining in the future. + +Asking for Changes +------------------ + +Rarely something is perfect from the start, while the code itself is good. +It may not be optimal or conform the Symfony coding style. + +Again, understand the author already spent time on the issue and asking +for (small) changes may be misinterpreted or seen as a personal attack. + +Be thankful for their work (so far), stay positive and really help them +to make the contribution a great one. *Especially if they are a first +time contributor.* + +Use words like "Please", "Thank you" and "Could you" instead of making demands; + +* "Thank you for your work so far. I left some suggestions for improvement + to make the code more readable." + +* "Your code contains some coding-style problems, can you fix these before + we merge? Thank you" + +* "Please use 4 spaces instead of tabs", "This needs be on the previous line"; + +During a pull request review you can usually leave more then one comment, +you don't have to use "Please" all the time. But it wouldn't hurt. + +It may not seem like much, but saying "Thank you" does make others feel +more welcome. + +Using Humor +----------- + +In short: Extreme misbehavior will not be tolerated and may even get you banned; +Keep it real and friendly. + +**Don't use sarcasm for a serious topic, that's not something that belongs +to the Symfony community.** And don't marginalize someone's problems; +``Well I guess that's not supposed to happen? 😆``. + +Even if someone's explanation is "inviting to joke about it", it's a real +problem to them. Making jokes about this doesn't help with solving their +problem and only makes them *feel stupid*. Instead try to discover what +the problem is really about. + +Final Words +----------- + +Don't feel bad if you "failed" to follow these tips. As long as your +intentions were good and you didn't really offend or insult anyone; +you can explain you misunderstood, you didn't mean to marginalize or +simply failed. + +But don't say it "just because", if your apology is not really meant +you *will* lose credibility and respect from other developers. + +*Do unto others as you would have them do unto you.* diff --git a/contributing/community/reviews.rst b/contributing/community/reviews.rst new file mode 100644 index 00000000000..2d4a5f125fa --- /dev/null +++ b/contributing/community/reviews.rst @@ -0,0 +1,216 @@ +Community Reviews +================= + +Symfony is an open-source project driven by a large community. If you don't feel +ready to contribute code or patches, reviewing issues and pull requests (PRs) +can be a great start to get involved and give back. In fact, people who "triage" +issues are the backbone to Symfony's success! + +Why Reviewing Is Important +-------------------------- + +Community reviews are essential for the development of the Symfony framework, +since there are many more pull requests and bug reports than there are members +in the Symfony core team to review, fix and merge them. + +On the `Symfony issue tracker`_, you can find many items in a `Needs Review`_ +status: + +* **Bug Reports**: Bug reports need to be checked for completeness. + Is any important information missing? Can the bug be *easily* reproduced? + +* **Pull Requests**: Pull requests contain code that fixes a bug or implements + new functionality. Reviews of pull requests ensure that they are implemented + properly, are covered by test cases, don't introduce new bugs and maintain + backward compatibility. + +Note that **anyone who has some basic familiarity with Symfony and PHP can +review bug reports and pull requests**. You don't need to be an expert to help. + +Be Constructive +--------------- + +Before you begin, remember that you are looking at the result of someone else's +hard work. A good review comment thanks the contributor for their work, +identifies what was done well, identifies what should be improved and suggests a +next step. + +Create a GitHub Account +----------------------- + +Symfony uses GitHub_ to manage bug reports and pull requests. If you want to +do reviews, you need to `create a GitHub account`_ and log in. + +The Bug Report Review Process +----------------------------- + +A good way to get started with reviewing is to pick a bug report from the +`bug reports in need of review`_. + +The steps for the review are: + +#. **Is the Report Complete?** + + Good bug reports contain a link to a fork of the `Symfony Standard Edition`_ + (the "reproduction project") that reproduces the bug. If it doesn't, the + report should at least contain enough information and code samples to + reproduce the bug. + +#. **Reproduce the Bug** + + Download the reproduction project and test whether the bug can be reproduced + on your system. If the reporter did not provide a reproduction project, + create one by forking_ the `Symfony Standard Edition`_. + +#. **Update the Issue Status** + + At last, add a comment to the bug report. **Thank the reporter for reporting + the bug**. Include the line ``Status: `` in your comment to trigger + our `Carson Bot`_ which updates the status label of the issue. You can set + the status to one of the following: + + **Needs Work** If the bug *does not* contain enough information to be + reproduced, explain what information is missing and move the report to this + status. + + **Works for me** If the bug *does* contain enough information to be + reproduced but works on your system, or if the reported bug is a feature and + not a bug, provide a short explanation and move the report to this status. + + **Reviewed** If you can reproduce the bug, move the report to this status. + If you created a reproduction project, include the link to the project in + your comment. + +.. topic:: Example + + Here is a sample comment for a bug report that could be reproduced: + + .. code-block:: text + + Thank you @weaverryan for creating this bug report! This indeed looks + like a bug. I reproduced the bug in the "kernel-bug" branch of + https://github.com/webmozart/symfony-standard. + + Status: Reviewed + +The Pull Request Review Process +------------------------------- + +The process for reviewing pull requests (PRs) is similar to the one for bug +reports. Reviews of pull requests usually take a little longer since you need +to understand the functionality that has been fixed or added and find out +whether the implementation is complete. + +It is okay to do partial reviews! If you do a partial review, comment how far +you got and leave the PR in "Needs Review" state. + +Pick a pull request from the `PRs in need of review`_ and follow these steps: + +#. **Is the PR Complete**? + + Every pull request must contain a header that gives some basic information + about the PR. You can find the template for that header in the + :ref:`Contribution Guidelines `. + +#. **Is the Base Branch Correct?** + + GitHub displays the branch that a PR is based on below the title of the + pull request. Is that branch correct? + + * Bugs should be fixed in the oldest, maintained version that contains the + bug. Check :doc:`Symfony's Release Schedule ` to find the oldest + currently supported version. + + * New features should always be added to the current development version. + Check the `Symfony Roadmap`_ to find the current development version. + +#. **Reproduce the Problem** + + Read the issue that the pull request is supposed to fix. Reproduce the + problem on a clean `Symfony Standard Edition`_ project and try to understand + why it exists. If the linked issue already contains such a project, install + it and run it on your system. + +#. **Review the Code** + + Read the code of the pull request and check it against some common criteria: + + * Does the code address the issue the PR is intended to fix/implement? + * Does the PR stay within scope to address *only* that issue? + * Does the PR contain automated tests? Do those tests cover all relevant + edge cases? + * Does the PR contain sufficient comments to easily understand its code? + * Does the code break backward compatibility? If yes, does the PR header say + so? + * Does the PR contain deprecations? If yes, does the PR header say so? Does + the code contain ``trigger_error()`` statements for all deprecated + features? + * Are all deprecations and backward compatibility breaks documented in the + latest UPGRADE-X.X.md file? Do those explanations contain "Before"/"After" + examples with clear upgrade instructions? + + .. note:: + + Eventually, some of these aspects will be checked automatically. + +#. **Test the Code** + + Take your project from step 3 and test whether the PR works properly. + Replace the Symfony project in the ``vendor`` directory by the code in the + PR by running the following Git commands. Insert the PR ID (that's the number + after the ``#`` in the PR title) for the ```` placeholders: + + .. code-block:: text + + $ cd vendor/symfony/symfony + $ git fetch origin pull//head:pr + $ git checkout pr + + For example: + + .. code-block:: text + + $ git fetch origin pull/15723/head:pr15723 + $ git checkout pr15723 + + Now you can :doc:`test the project ` against + the code in the PR. + +#. **Update the PR Status** + + At last, add a comment to the PR. **Thank the contributor for working on the + PR**. Include the line ``Status: `` in your comment to trigger our + `Carson Bot`_ which updates the status label of the issue. You can set the + status to one of the following: + + **Needs Work** If the PR is not yet ready to be merged, explain the issues + that you found and move it to this status. + + **Reviewed** If the PR satisfies all the checks above, move it to this + status. A core contributor will soon look at the PR and decide whether it can + be merged or needs further work. + +.. topic:: Example + + Here is a sample comment for a PR that is not yet ready for merge: + + .. code-block:: text + + Thank you @weaverryan for working on this! It seems that your test + cases don't cover the cases when the counter is zero or smaller. + Could you please add some tests for that? + + Status: Needs Work + +.. _GitHub: https://github.com +.. _Symfony issue tracker: https://github.com/symfony/symfony/issues +.. _Symfony Standard Edition: https://github.com/symfony/symfony-standard +.. _create a GitHub account: https://help.github.com/articles/signing-up-for-a-new-github-account/ +.. _forking: https://help.github.com/articles/fork-a-repo/ +.. _bug reports in need of review: https://github.com/symfony/symfony/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A%22Bug%22+label%3A%22Status%3A+Needs+Review%22+ +.. _PRs in need of review: https://github.com/symfony/symfony/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Apr+label%3A%22Status%3A+Needs+Review%22+ +.. _Contribution Guidelines: https://github.com/symfony/symfony/blob/master/CONTRIBUTING.md +.. _Symfony's Release Schedule: https://symfony.com/doc/current/contributing/community/releases.html#schedule +.. _Symfony Roadmap: https://symfony.com/roadmap +.. _Carson Bot: https://github.com/carsonbot/carsonbot +.. _`Needs Review`: https://github.com/symfony/symfony/labels/Status%3A%20Needs%20Review diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index ce50f4fc974..56fcbba574d 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -1,8 +1,8 @@ Documentation Format ==================== -The Symfony documentation uses reStructuredText_ as its markup language and -Sphinx_ for generating the documentation in the formats read by the end users, +The Symfony documentation uses `reStructuredText`_ as its markup language and +`Sphinx`_ for generating the documentation in the formats read by the end users, such as HTML and PDF. reStructuredText @@ -27,7 +27,7 @@ tutorial and the `reStructuredText Reference`_. Sphinx ------ -Sphinx is a build system that provides tools to create documentation from +Sphinx_ is a build system that provides tools to create documentation from reStructuredText documents. As such, it adds new directives and interpreted text roles to the standard reST markup. Read more about the `Sphinx Markup Constructs`_. @@ -99,8 +99,8 @@ Markup Format Use It to Display ``xml`` XML ``php`` PHP ``yaml`` YAML -``jinja`` Pure Twig markup -``html+jinja`` Twig markup blended with HTML +``twig`` Pure Twig markup +``html+twig`` Twig markup blended with HTML ``html+php`` PHP code blended with HTML ``ini`` INI ``php-annotations`` PHP Annotations @@ -120,18 +120,18 @@ The page name should not include the file extension (``.rst``). For example: .. code-block:: rst - :doc:`/book/controller` + :doc:`/controller` - :doc:`/components/event_dispatcher/introduction` + :doc:`/components/event_dispatcher` - :doc:`/cookbook/configuration/environments` + :doc:`/configuration/environments` The title of the linked page will be automatically used as the text of the link. If you want to modify that title, use this alternative syntax: .. code-block:: rst - :doc:`Spooling Email ` + :doc:`Spooling Email ` .. note:: @@ -143,7 +143,7 @@ If you want to modify that title, use this alternative syntax: :doc:`controller` - :doc:`event_dispatcher/introduction` + :doc:`event_dispatcher` :doc:`environments` @@ -177,8 +177,8 @@ Symfony, you should precede your description of the change with a .. code-block:: rst - .. versionadded:: 2.3 - The ``askHiddenResponse`` method was introduced in Symfony 2.3. + .. versionadded:: 2.7 + The ``askHiddenResponse()`` method was introduced in Symfony 2.7. You can also ask a question and hide the response. This is particularly [...] @@ -187,30 +187,16 @@ how the behavior has changed: .. code-block:: rst - .. versionadded:: 2.3 + .. versionadded:: 2.7 The ``include()`` function is a new Twig feature that's available in - Symfony 2.3. Prior, the ``{% include %}`` tag was used. + Symfony 2.7. Prior, the ``{% include %}`` tag was used. Whenever a new minor version of Symfony is released (e.g. 2.4, 2.5, etc), a new branch of the documentation is created from the ``master`` branch. At this point, all the ``versionadded`` tags for Symfony versions that have -reached end-of-life will be removed. For example, if Symfony 2.5 were released -today, and 2.2 had recently reached its end-of-life, the 2.2 ``versionadded`` -tags would be removed from the new ``2.5`` branch. - -Testing Documentation -~~~~~~~~~~~~~~~~~~~~~ - -When submitting a new content to the documentation repository or when changing -any existing resource, an automatic process will check if your documentation is -free of syntax errors and is ready to be reviewed. - -Nevertheless, if you prefer to do this check locally on your own machine before -submitting your documentation, follow these steps: - -* Install Sphinx_; -* Install the Sphinx extensions using git submodules: ``$ git submodule update --init``; -* Run ``make html`` and view the generated HTML in the ``_build/html`` directory. +reached end-of-maintenance will be removed. For example, if Symfony 2.5 were +released today, and 2.2 had recently reached its end-of-maintenance, the 2.2 +``versionadded`` tags would be removed from the new ``2.5`` branch. .. _reStructuredText: http://docutils.sourceforge.net/rst.html .. _Sphinx: http://sphinx-doc.org/ diff --git a/contributing/documentation/index.rst b/contributing/documentation/index.rst index 08782431605..d4ccfe74310 100644 --- a/contributing/documentation/index.rst +++ b/contributing/documentation/index.rst @@ -1,11 +1,31 @@ Contributing Documentation ========================== +These short articles explain everything you need to contribute to the Symfony +documentation: + +:doc:`The Contribution Process ` + Explains the steps to follow to contribute fixes and new contents. It's the + same contribution process followed by most open source projects, so you may + already know everything that is needed. + +:doc:`Documentation Formats ` + Explains the technical details of the reStructuredText format that is used to + write the docs. Skip it if you are already familiar with this format. + +:doc:`Documentation Standards ` + Explains how to write docs and code examples to match the style and tone of + the rest of the existing documentation. + +:doc:`License ` + Explains the details of the Creative Commons BY-SA 3.0 license used for the + Symfony Documentation. + .. toctree:: - :maxdepth: 2 + :hidden: - overview format + license + overview standards translations - license diff --git a/contributing/documentation/license.rst b/contributing/documentation/license.rst index d8a23b0efbe..d786da396cd 100644 --- a/contributing/documentation/license.rst +++ b/contributing/documentation/license.rst @@ -48,5 +48,12 @@ Attribution-Share Alike 3.0 Unported License (`CC BY-SA 3.0`_). This is a human-readable summary of the `Legal Code (the full license)`_. +Other Symfony Licenses +---------------------- + +Check out the :doc:`license of the Symfony code ` +and other `Symfony licenses and trademarks`_. + .. _`CC BY-SA 3.0`: http://creativecommons.org/licenses/by-sa/3.0/ .. _Legal Code (the full license): http://creativecommons.org/licenses/by-sa/3.0/legalcode +.. _`Symfony licenses and trademarks`: https://symfony.com/license diff --git a/contributing/documentation/overview.rst b/contributing/documentation/overview.rst index fd101ae7d08..4029ebc73ee 100644 --- a/contributing/documentation/overview.rst +++ b/contributing/documentation/overview.rst @@ -1,28 +1,58 @@ Contributing to the Documentation ================================= -One of the essential principles of the Symfony project is that **documentation is -as important as code**. That's why a great amount of resources are dedicated to -documenting new features and to keeping the rest of the documentation up-to-date. - -More than 700 developers all around the world have contributed to Symfony's -documentation and we are glad that you are considering joining this big family. -This guide will explain everything you need to contribute to the Symfony -documentation. - Before Your First Contribution ------------------------------ -**Before contributing**, you should consider the following: +**Before contributing**, you need to: + +* Sign up for a free `GitHub`_ account, which is the service where the Symfony + documentation is hosted. +* Be familiar with the `reStructuredText`_ markup language, which is used to + write Symfony docs. Read :doc:`this article ` + for a quick overview. + +.. _minor-changes-e-g-typos: + +Fast Online Contributions +------------------------- + +If you're making a relatively small change - like fixing a typo or rewording +something - the easiest way to contribute is directly on GitHub! You can do this +while you're reading the Symfony documentation. + +**Step 1.** Click on the **edit this page** button on the upper right corner +and you'll be redirected to GitHub: + +.. image:: /_images/contributing/docs-github-edit-page.png + :align: center + :class: with-browser + +**Step 2.** Edit the contents, describe your changes and click on the +**Propose file change** button. -* Symfony documentation is written using reStructuredText_ markup language. - If you are not familiar with this format, read :doc:`this article ` - for a quick overview of its basic features. -* Symfony documentation is hosted on GitHub_. You'll need a GitHub user account - to contribute to the documentation. -* Symfony documentation is published under a - :doc:`Creative Commons BY-SA 3.0 License ` - and all your contributions will implicitly adhere to that license. +**Step 3.** GitHub will now create a branch and a commit for your changes +(forking the repository first if this is your first contribution) and it will +also display a preview of your changes: + +.. image:: /_images/contributing/docs-github-create-pr.png + :align: center + :class: with-browser + +If everything is correct, click on the **Create pull request** button. + +**Step 4.** GitHub will display a new page where you can do some last-minute +changes to your pull request before creating it. For simple contributions, you +can safely ignore these options and just click on the **Create pull request** +button again. + +**Congratulations!** You just created a pull request to the official Symfony +documentation! The community will now review your pull request and (possibly) +suggest tweaks. + +If your contribution is large or if you prefer to work on your own computer, +keep reading this guide to learn an alternative way to send pull requests to the +Symfony Documentation. Your First Documentation Contribution ------------------------------------- @@ -31,258 +61,214 @@ In this section, you'll learn how to contribute to the Symfony documentation for the first time. The next section will explain the shorter process you'll follow in the future for every contribution after your first one. -Let's imagine that you want to improve the installation chapter of the Symfony -book. In order to make your changes, follow these steps: +Let's imagine that you want to improve the Setup guide. In order to make your +changes, follow these steps: **Step 1.** Go to the official Symfony documentation repository located at -`github.com/symfony/symfony-docs`_ and `fork the repository`_ to your personal -account. This is only needed the first time you contribute to Symfony. +`github.com/symfony/symfony-docs`_ and click on the **Fork** button to `fork the +repository`_ to your personal account. This is only needed the first time you +contribute to Symfony. -**Step 2.** **Clone** the forked repository to your local machine (this -example uses the ``projects/symfony-docs/`` directory to store the documentation; -change this value accordingly): +**Step 2.** **Clone** the forked repository to your local machine (this example +uses the ``projects/symfony-docs/`` directory to store the documentation; change +this value accordingly): -.. code-block:: bash +.. code-block:: terminal $ cd projects/ - $ git clone git://github.com//symfony-docs.git + $ git clone git://github.com/YOUR-GITHUB-USERNAME/symfony-docs.git -**Step 3.** Switch to the **oldest maintained branch** before making any change. -Nowadays this is the ``2.3`` branch: +**Step 3.** Add the original Symfony docs repository as a "Git remote" executing +this command: -.. code-block:: bash +.. code-block:: terminal $ cd symfony-docs/ - $ git checkout 2.3 + $ git remote add upstream https://github.com/symfony/symfony-docs.git -If you are instead documenting a new feature, switch to the first Symfony -version which included it: ``2.5``, ``2.6``, etc. +If things went right, you'll see the following when listing the "remotes" of +your project: -**Step 4.** Create a dedicated **new branch** for your changes. This greatly -simplifies the work of reviewing and merging your changes. Use a short and -memorable name for the new branch: +.. code-block:: terminal -.. code-block:: bash + $ git remote -v + origin git@github.com:YOUR-GITHUB-USERNAME/symfony-docs.git (fetch) + origin git@github.com:YOUR-GITHUB-USERNAME/symfony-docs.git (push) + upstream https://github.com/symfony/symfony-docs.git (fetch) + upstream https://github.com/symfony/symfony-docs.git (push) - $ git checkout -b improve_install_chapter +Fetch all the commits of the upstream branches by executing this command: + +.. code-block:: terminal + + $ git fetch upstream + +The purpose of this step is to allow you work simultaneously on the official +Symfony repository and on your own fork. You'll see this in action in a moment. + +**Step 4.** Create a dedicated **new branch** for your changes. Use a short and +memorable name for the new branch (if you are fixing a reported issue, use +``fix_XXX`` as the branch name, where ``XXX`` is the number of the issue): + +.. code-block:: terminal + + $ git checkout -b improve_install_article upstream/2.7 + +In this example, the name of the branch is ``improve_install_article`` and the +``upstream/2.7`` value tells Git to create this branch based on the ``2.7`` +branch of the ``upstream`` remote, which is the original Symfony Docs repository. + +Fixes should always be based on the **oldest maintained branch** which contains +the error. Nowadays this is the ``2.7`` branch. If you are instead documenting a +new feature, switch to the first Symfony version that included it, e.g. +``upstream/3.1``. Not sure? That's ok! Just use the ``upstream/master`` branch. **Step 5.** Now make your changes in the documentation. Add, tweak, reword and -even remove any content, but make sure that you comply with the -:doc:`/contributing/documentation/standards`. +even remove any content and do your best to comply with the +:doc:`/contributing/documentation/standards`. Then commit your changes! + +.. code-block:: terminal + + # if the modified content existed before + $ git add setup.rst + $ git commit setup.rst **Step 6.** **Push** the changes to your forked repository: -.. code-block:: bash +.. code-block:: terminal - $ git commit book/installation.rst - $ git push origin improve_install_chapter + $ git push origin improve_install_article + +The ``origin`` value is the name of the Git remote that corresponds to your +forked repository and ``improve_install_article`` is the name of the branch you +created previously. **Step 7.** Everything is now ready to initiate a **pull request**. Go to your -forked repository at ``https//github.com//symfony-docs`` +forked repository at ``https://github.com/YOUR-GITHUB-USERNAME/symfony-docs`` and click on the **Pull Requests** link located in the sidebar. Then, click on the big **New pull request** button. As GitHub cannot guess the exact changes that you want to propose, select the appropriate branches where changes should be applied: -.. image:: /images/contributing/docs-pull-request-change-base.png +.. image:: /_images/contributing/docs-pull-request-change-base.png :align: center In this example, the **base fork** should be ``symfony/symfony-docs`` and -the **base** branch should be the ``2.3``, which is the branch that you selected +the **base** branch should be the ``2.7``, which is the branch that you selected to base your changes on. The **head fork** should be your forked copy -of ``symfony-docs`` and the **compare** branch should be ``improve_install_chapter``, +of ``symfony-docs`` and the **compare** branch should be ``improve_install_article``, which is the name of the branch you created and where you made your changes. .. _pull-request-format: **Step 8.** The last step is to prepare the **description** of the pull request. -To ensure that your work is reviewed quickly, please add the following table -at the beginning of your pull request description: - -.. code-block:: text - - | Q | A - | ------------- | --- - | Doc fix? | [yes|no] - | New docs? | [yes|no] (PR # on symfony/symfony if applicable) - | Applies to | [Symfony version numbers this applies to] - | Fixed tickets | [comma separated list of tickets fixed by the PR] - -In this example, this table would look as follows: - -.. code-block:: text +A short phrase or paragraph describing the proposed changes is enough to ensure +that your contribution can be reviewed. - | Q | A - | ------------- | --- - | Doc fix? | yes - | New docs? | no - | Applies to | all - | Fixed tickets | #10575 +**Step 9.** Now that you've successfully submitted your first contribution to +the Symfony documentation, **go and celebrate!** The documentation managers +will carefully review your work in short time and they will let you know about +any required change. -**Step 9.** Now that you've successfully submitted your first contribution to the -Symfony documentation, **go and celebrate!** The documentation managers will -carefully review your work in short time and they will let you know about any -required change. +In case you are asked to add or modify something, don't create a new pull +request. Instead, make sure that you are on the correct branch, make your +changes and push the new changes: -In case you need to add or modify anything, there is no need to create a new -pull request. Just make sure that you are on the correct branch, make your -changes and push them: - -.. code-block:: bash +.. code-block:: terminal $ cd projects/symfony-docs/ - $ git checkout improve_install_chapter + $ git checkout improve_install_article # ... do your changes $ git push -**Step 10.** After your pull request is eventually accepted and merged in the Symfony -documentation, you will be included in the `Symfony Documentation Contributors`_ -list. Moreover, if you happen to have a SensioLabsConnect_ profile, you will -get a cool `Symfony Documentation Badge`_. - -Your Second Documentation Contribution --------------------------------------- - -The first contribution took some time because you had to fork the repository, -learn how to write documentation, comply with the pull requests standards, etc. -The second contribution will be much easier, except for one detail: given the -furious update activity of the Symfony documentation repository, odds are that -your fork is now out of date with the official repository. - -Solving this problem requires you to `sync your fork`_ with the original repository. -To do this, execute this command first to tell git about the original repository: - -.. code-block:: bash - - $ cd projects/symfony-docs/ - $ git remote add upstream https://github.com/symfony/symfony-docs.git - -Now you can **sync your fork** by executing the following command: - -.. code-block:: bash - - $ cd projects/symfony-docs/ - $ git fetch upstream - $ git checkout 2.3 - $ git merge upstream/2.3 - -This command will update the ``2.3`` branch, which is the one you used to -create the new branch for your changes. If you have used another base branch, -e.g. ``master``, replace the ``2.3`` with the appropriate branch name. - -Great! Now you can proceed by following the same steps explained in the previous -section: - -.. code-block:: bash - - # create a new branch to store your changes based on the 2.3 branch - $ cd projects/symfony-docs/ - $ git checkout 2.3 - $ git checkout -b my_changes - - # ... do your changes - - # submit the changes to your forked repository - $ git add xxx.rst # (optional) only if this is a new content - $ git commit xxx.rst - $ git push origin my_changes - - # go to GitHub and create the Pull Request - # - # Include this table in the description: - # | Q | A - # | ------------- | --- - # | Doc fix? | [yes|no] - # | New docs? | [yes|no] (PR # on symfony/symfony if applicable) - # | Applies to | [Symfony version numbers this applies to] - # | Fixed tickets | [comma separated list of tickets fixed by the PR] - -Your second contribution is now complete, so **go and celebrate again!** -You can also see how your ranking improves in the list of -`Symfony Documentation Contributors`_. +**Step 10.** After your pull request is eventually accepted and merged in the +Symfony documentation, you will be included in the `Symfony Documentation +Contributors`_ list. Moreover, if you happen to have a `SensioLabsConnect`_ +profile, you will get a cool `Symfony Documentation Badge`_. Your Next Documentation Contributions ------------------------------------- -Now that you've made two contributions to the Symfony documentation, you are -probably comfortable with all the Git-magic involved in the process. That's -why your next contributions would be much faster. Here you can find the complete -steps to contribute to the Symfony documentation, which you can use as a -**checklist**: +Check you out! You've made your first contribution to the Symfony documentation! +Somebody throw a party! Your first contribution took a little extra time because +you needed to learn a few standards and setup your computer. But from now on, +your contributions will be much easier to complete. -.. code-block:: bash +Here is a **checklist** of steps that will guide you through your next +contribution to the Symfony docs: - # sync your fork with the official Symfony repository +.. code-block:: terminal + + # create a new branch based on the oldest maintained version $ cd projects/symfony-docs/ $ git fetch upstream - $ git checkout 2.3 - $ git merge upstream/2.3 - - # create a new branch from the oldest maintained version - $ git checkout 2.3 - $ git checkout -b my_changes + $ git checkout -b my_changes upstream/2.7 # ... do your changes - # add and commit your changes - $ git add xxx.rst # (optional) only if this is a new content + # (optional) add your changes if this is a new content + $ git add xxx.rst + + # commit your changes and push them to your fork $ git commit xxx.rst $ git push origin my_changes - # go to GitHub and create the Pull Request - # - # Include this table in the description: - # | Q | A - # | ------------- | --- - # | Doc fix? | [yes|no] - # | New docs? | [yes|no] (PR # on symfony/symfony if applicable) - # | Applies to | [Symfony version numbers this applies to] - # | Fixed tickets | [comma separated list of tickets fixed by the PR] + # ... go to GitHub and create the Pull Request # (optional) make the changes requested by reviewers and commit them $ git commit xxx.rst $ git push -You guessed right: after all this hard work, it's **time to celebrate again!** - +After completing your next contributions, also watch your ranking improve on +the list of `Symfony Documentation Contributors`_. You guessed right: after all +this hard work, it's **time to celebrate again!** Review your changes ------------------- Every GitHub Pull Request is automatically built and deployed by `Platform.sh`_ -on a single environment that you can access on your browser to review your +on a single environment that you can access on your browser to review your changes. -.. image:: /images/contributing/docs-pull-request-platformsh.png +.. image:: /_images/contributing/docs-pull-request-platformsh.png :align: center :alt: Platform.sh Pull Request Deployment -To access the `Platform.sh`_ environment URL, simply go to your Pull Request -page on GitHub and click on ``Details``. +To access the `Platform.sh`_ environment URL, go to your Pull Request page on +GitHub, click on the **Show all checks** link and finally, click on the ``Details`` +link displayed for Platform.sh service. .. note:: - The specific configuration files at the root of the Git repository: - ``.platform.app.yaml``, ``.platform/services.yaml`` and - ``.platform/routes.yaml`` allow `Platform.sh`_ to build Pull Requests. + Only Pull Requests to maintained branches are automatically built by + Platform.sh. Check the `roadmap`_ for maintained branches. -Minor Changes (e.g. Typos) --------------------------- +Build the Documentation Locally +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Alternatively you can build the documentation on your own computer for testing +purposes following these steps: -You may find just a typo and want to fix it. Due to GitHub's functional -frontend, it is quite simple to create Pull Requests right in your -browser while reading the docs on symfony.com. To do this, just click -the **edit this page** button on the upper right corner. Beforehand, -please switch to the right branch as mentioned before. Now you are able -to edit the content and describe your changes within the GitHub -frontend. When your work is done, click **Propose file change** to -create a commit and, in case it is your first contribution, also your -fork. A new branch is created automatically in order to provide a base -for your Pull Request. Then fill out the form to create the Pull Request -as described above. +#. Install `pip`_ as explained in the `pip installation`_ article; + +#. Install `Sphinx`_ and `Sphinx Extensions for PHP and Symfony`_ + (depending on your system, you may need to execute this command as root user): + + .. code-block:: terminal + + $ pip install sphinx~=1.3.0 git+https://github.com/fabpot/sphinx-php.git + +#. Run the following command to build the documentation in HTML format: + + .. code-block:: terminal + + $ cd _build/ + $ make html + +The generated documentation is available in the ``_build/html`` directory. Frequently Asked Questions -------------------------- @@ -294,11 +280,6 @@ Please be patient. It can take up to several days before your pull request can be fully reviewed. After merging the changes, it could take again several hours before your changes appear on the symfony.com website. -What If I Want to Translate Some Documentation into my Language? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Read the dedicated :doc:`document `. - Why Should I Use the Oldest Maintained Branch Instead of the Master Branch? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -307,8 +288,8 @@ into multiple branches, corresponding to the different versions of Symfony itsel The ``master`` branch holds the documentation for the development branch of the code. -Unless you're documenting a feature that was introduced after Symfony 2.3, -your changes should always be based on the ``2.3`` branch. Documentation managers +Unless you're documenting a feature that was introduced after Symfony 2.7, +your changes should always be based on the ``2.7`` branch. Documentation managers will use the necessary Git-magic to also apply your changes to all the active branches of the documentation. @@ -338,11 +319,16 @@ your proposal after you put all that hard work into making the changes. We definitely don't want you to waste your time! .. _`github.com/symfony/symfony-docs`: https://github.com/symfony/symfony-docs -.. _reStructuredText: http://docutils.sourceforge.net/rst.html -.. _GitHub: https://github.com/ +.. _`reStructuredText`: http://docutils.sourceforge.net/rst.html +.. _`GitHub`: https://github.com/ .. _`fork the repository`: https://help.github.com/articles/fork-a-repo .. _`Symfony Documentation Contributors`: https://symfony.com/contributors/doc -.. _SensioLabsConnect: https://connect.sensiolabs.com/ +.. _`SensioLabsConnect`: https://connect.sensiolabs.com/ .. _`Symfony Documentation Badge`: https://connect.sensiolabs.com/badge/36/symfony-documentation-contributor .. _`sync your fork`: https://help.github.com/articles/syncing-a-fork -.. _`Platform.sh`: https://platform.sh \ No newline at end of file +.. _`Platform.sh`: https://platform.sh +.. _`roadmap`: https://symfony.com/roadmap +.. _`pip`: https://pip.pypa.io/en/stable/ +.. _`pip installation`: https://pip.pypa.io/en/stable/installing/ +.. _`Sphinx`: http://sphinx-doc.org/ +.. _`Sphinx Extensions for PHP and Symfony`: https://github.com/fabpot/sphinx-php diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index 713ca038e88..b0471945feb 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -1,8 +1,8 @@ Documentation Standards ======================= -In order to help the reader as much as possible and to create code examples that -look and feel familiar, you should follow these standards. +Contributions must follow these standards to match the style and tone of the +rest of the Symfony documentation. Sphinx ------ @@ -13,8 +13,8 @@ Sphinx * Each line should break approximately after the first word that crosses the 72nd character (so most lines end up being 72-78 characters); * The ``::`` shorthand is *preferred* over ``.. code-block:: php`` to begin a PHP - code block (read `the Sphinx documentation`_ to see when you should use the - shorthand); + code block unless it results in the marker being on its own line (read + `the Sphinx documentation`_ to see when you should use the shorthand); * Inline hyperlinks are **not** used. Separate the link and their target definition, which you add on the bottom of the page; * Inline markup should be closed on the same line as the open-string; @@ -178,8 +178,8 @@ In addition, documentation follows these rules: .. _`the Sphinx documentation`: http://sphinx-doc.org/rest.html#source-code .. _`Twig Coding Standards`: http://twig.sensiolabs.org/doc/coding_standards.html .. _`reserved by the IANA`: http://tools.ietf.org/html/rfc2606#section-3 -.. _`American English`: http://en.wikipedia.org/wiki/American_English -.. _`American English Oxford Dictionary`: http://www.oxforddictionaries.com/definition/american_english/ -.. _`headings and titles`: http://en.wikipedia.org/wiki/Letter_case#Headings_and_publication_titles -.. _`Serial (Oxford) Commas`: http://en.wikipedia.org/wiki/Serial_comma -.. _`nosism`: http://en.wikipedia.org/wiki/Nosism +.. _`American English`: https://en.wikipedia.org/wiki/American_English +.. _`American English Oxford Dictionary`: http://en.oxforddictionaries.com/definition/american_english/ +.. _`headings and titles`: https://en.wikipedia.org/wiki/Letter_case#Headings_and_publication_titles +.. _`Serial (Oxford) Commas`: https://en.wikipedia.org/wiki/Serial_comma +.. _`nosism`: https://en.wikipedia.org/wiki/Nosism diff --git a/contributing/documentation/translations.rst b/contributing/documentation/translations.rst index f6951d228c4..5ebdecd41e2 100644 --- a/contributing/documentation/translations.rst +++ b/contributing/documentation/translations.rst @@ -1,92 +1,14 @@ Translations ============ -The Symfony documentation is written in English and many people are involved -in the translation process. +The official Symfony documentation is published only in English. You can +read about the reasons in `this blog post`_. -.. note:: +We have taken steps to improve the experience when using +`Google Translate`_ to prevent code blocks from being translated. - Symfony Project officially discourages from starting new translations for the - documentation. As a matter of fact, there is `an ongoing discussion`_ in - the community about the benefits and drawbacks of community driven translations. +To translate any page in our documentation please copy any URL from the +documentation and paste it into the form on the Google Translate site. -Contributing ------------- - -First, become familiar with the :doc:`markup language ` -used by the documentation. - -Then, subscribe to the `Symfony docs mailing-list`_, as collaboration happens -there. - -Finally, find the *master* repository for the language you want to contribute -for. Here is the list of the official *master* repositories: - -* *English*: https://github.com/symfony/symfony-docs -* *French*: https://github.com/symfony-fr/symfony-docs-fr -* *Italian*: https://github.com/garak/symfony-docs-it -* *Japanese*: https://github.com/symfony-japan/symfony-docs-ja -* *Portuguese (Brazilian)*: https://github.com/andreia/symfony-docs-pt-BR - -.. note:: - - If you want to contribute translations for a new language, read the - :ref:`dedicated section `. - -Joining the Translation Team ----------------------------- - -If you want to help translating some documents for your language or fix some -bugs, consider joining us; it's a very easy process: - -* Introduce yourself on the `Symfony docs mailing-list`_; -* *(optional)* Ask which documents you can work on; -* Fork the *master* repository for your language (click the "Fork" button on - the GitHub page); -* Translate some documents; -* Ask for a pull request (click on the "Pull Request" from your page on - GitHub); -* The team manager accepts your modifications and merges them into the master - repository; -* The documentation website is updated every other night from the master - repository. - -.. _translations-adding-a-new-language: - -Adding a new Language ---------------------- - -This section gives some guidelines for starting the translation of the -Symfony documentation for a new language. - -As starting a translation is a lot of work, talk about your plan on the -`Symfony docs mailing-list`_ and try to find motivated people willing to help. - -When the team is ready, nominate a team manager; they will be responsible for -the *master* repository. - -Create the repository and copy the *English* documents. - -The team can now start the translation process. - -When the team is confident that the repository is in a consistent and stable -state (everything is translated, or non-translated documents have been removed -from the toctrees -- files named ``index.rst`` and ``map.rst.inc``), the team -manager can ask that the repository is added to the list of official *master* -repositories by sending an email to Fabien (fabien at symfony.com). - -Maintenance ------------ - -Translation does not end when everything is translated. The documentation is a -moving target (new documents are added, bugs are fixed, paragraphs are -reorganized, ...). The translation team need to closely follow the English -repository and apply changes to the translated documents as soon as possible. - -.. caution:: - - Non maintained languages are removed from the official list of - repositories as obsolete documentation is dangerous. - -.. _`an ongoing discussion`: https://github.com/symfony/symfony-docs/issues/4078 -.. _Symfony docs mailing-list: http://groups.google.com/group/symfony-docs +.. _`this blog post`: https://symfony.com/blog/discontinuing-the-symfony-community-translations +.. _`Google Translate`: https://translate.google.com diff --git a/contributing/index.rst b/contributing/index.rst index a3177b959f0..ee61fb73bd2 100644 --- a/contributing/index.rst +++ b/contributing/index.rst @@ -4,8 +4,10 @@ Contributing .. toctree:: :hidden: + code_of_conduct/index code/index documentation/index community/index + code_of_conduct/index .. include:: /contributing/map.rst.inc diff --git a/contributing/map.rst.inc b/contributing/map.rst.inc index 84344670d90..15775643c62 100644 --- a/contributing/map.rst.inc +++ b/contributing/map.rst.inc @@ -1,11 +1,20 @@ +* **Code of Conduct** + + * :doc:`/contributing/code_of_conduct/code_of_conduct` + * :doc:`/contributing/code_of_conduct/reporting_guidelines` + * :doc:`/contributing/code_of_conduct/care_team` + * :doc:`/contributing/code_of_conduct/concrete_example_document` + * **Code** * :doc:`Bugs ` * :doc:`Patches ` + * :doc:`Reviewing Issues and Patches ` + * :doc:`Maintenance ` * :doc:`The Core Team ` * :doc:`Security ` * :doc:`Tests ` - * :doc:`Backwards Compatibility ` + * :doc:`Backward Compatibility ` * :doc:`Coding Standards` * :doc:`Code Conventions` * :doc:`Git` @@ -16,10 +25,11 @@ * :doc:`Overview ` * :doc:`Format ` * :doc:`Documentation Standards ` - * :doc:`Translations ` * :doc:`License ` * **Community** * :doc:`Release Process ` + * :doc:`Respectful Review comments ` + * :doc:`Community Reviews ` * :doc:`Other Resources ` diff --git a/controller.rst b/controller.rst new file mode 100644 index 00000000000..b70b6bb149e --- /dev/null +++ b/controller.rst @@ -0,0 +1,596 @@ +.. index:: + single: Controller + +Controller +========== + +A controller is a PHP function you create that reads information from the Symfony's +``Request`` object and creates and returns a ``Response`` object. The response could +be an HTML page, JSON, XML, a file download, a redirect, a 404 error or anything +else you can dream up. The controller executes whatever arbitrary logic +*your application* needs to render the content of a page. + +See how simple this is by looking at a Symfony controller in action. +This renders a page that prints a lucky (random) number:: + + // src/AppBundle/Controller/LuckyController.php + namespace AppBundle\Controller; + + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use Symfony\Component\HttpFoundation\Response; + + class LuckyController + { + /** + * @Route("/lucky/number") + */ + public function numberAction() + { + $number = mt_rand(0, 100); + + return new Response( + 'Lucky number: '.$number.'' + ); + } + } + +But in the real world, your controller will probably do a lot of work in order to +create the response. It might read information from the request, load a database +resource, send an email or set information on the user's session. +But in all cases, the controller will eventually return the ``Response`` object +that will be delivered back to the client. + +.. tip:: + + If you haven't already created your first working page, check out + :doc:`/page_creation` and then come back! + +.. index:: + single: Controller; Simple example + +A Simple Controller +------------------- + +While a controller can be any PHP callable (a function, method on an object, +or a ``Closure``), a controller is usually a method inside a controller +class:: + + // src/AppBundle/Controller/LuckyController.php + namespace AppBundle\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + + class LuckyController + { + /** + * @Route("/lucky/number/{max}") + */ + public function numberAction($max) + { + $number = mt_rand(0, $max); + + return new Response( + 'Lucky number: '.$number.'' + ); + } + } + +The controller is the ``numberAction()`` method, which lives inside a +controller class ``LuckyController``. + +This controller is pretty straightforward: + +* *line 2*: Symfony takes advantage of PHP's namespace functionality to + namespace the entire controller class. + +* *line 4*: Symfony again takes advantage of PHP's namespace functionality: + the ``use`` keyword imports the ``Response`` class, which the controller + must return. + +* *line 7*: The class can technically be called anything - but should end in the + word ``Controller`` (this isn't *required*, but some shortcuts rely on this). + +* *line 12*: Each action method in a controller class is suffixed with ``Action`` + (again, this isn't *required*, but some shortcuts rely on this). This method + is allowed to have a ``$max`` argument thanks to the ``{max}`` + :doc:`wildcard in the route `. + +* *line 16*: The controller creates and returns a ``Response`` object. + +.. index:: + single: Controller; Routes and controllers + +Mapping a URL to a Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to *view* the result of this controller, you need to map a URL to it via +a route. This was done above with the ``@Route("/lucky/number/{max}")`` annotation. + +To see your page, go to this URL in your browser: + + http://localhost:8000/lucky/number/100 + +For more information on routing, see :doc:`/routing`. + +.. index:: + single: Controller; Base controller class + +The Base Controller Class & Services +------------------------------------ + +For convenience, Symfony comes with an optional base +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class. +If you extend it, this won't change anything about how your controller +works, but you'll get access to a number of **helper methods** and the +**service container** (see :ref:`controller-accessing-services`): an +array-like object that gives you access to every useful object in the +system. These useful objects are called **services**, and Symfony ships +with a service object that can render Twig templates, another that can +log messages and many more. + +Add the ``use`` statement atop the ``Controller`` class and then modify +``LuckyController`` to extend it:: + + // src/AppBundle/Controller/LuckyController.php + namespace AppBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + + class LuckyController extends Controller + { + // ... + } + +Helper methods are just shortcuts to using core Symfony functionality +that's available to you with or without the use of the base +``Controller`` class. A great way to see the core functionality in +action is to look in the +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class. + +.. index:: + single: Controller; Redirecting + +Generating URLs +~~~~~~~~~~~~~~~ + +The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::generateUrl` +method is just a helper method that generates the URL for a given route:: + + $url = $this->generateUrl('blog_show', array('slug' => 'slug-value')); + +Redirecting +~~~~~~~~~~~ + +If you want to redirect the user to another page, use the ``redirectToRoute()`` +and ``redirect()`` methods:: + + public function indexAction() + { + // redirects to the "homepage" route + return $this->redirectToRoute('homepage'); + + // does a permanent - 301 redirect + return $this->redirectToRoute('homepage', array(), 301); + + // redirects to a route with parameters + return $this->redirectToRoute('blog_show', array('slug' => 'my-page')); + + // redirects externally + return $this->redirect('http://symfony.com/doc'); + } + +.. versionadded:: 2.6 + The ``redirectToRoute()`` method was introduced in Symfony 2.6. Previously (and still now), you + could use ``redirect()`` and ``generateUrl()`` together for this. + +For more information, see the :doc:`Routing article `. + +.. caution:: + + The ``redirect()`` method does not check its destination in any way. If you + redirect to some URL provided by the end-users, your application may be open + to the `unvalidated redirects security vulnerability`_. + +.. tip:: + + The ``redirectToRoute()`` method is simply a shortcut that creates a + ``Response`` object that specializes in redirecting the user. It's + equivalent to:: + + use Symfony\Component\HttpFoundation\RedirectResponse; + + public function indexAction() + { + return new RedirectResponse($this->generateUrl('homepage')); + } + +.. index:: + single: Controller; Rendering templates + +.. _controller-rendering-templates: + +Rendering Templates +~~~~~~~~~~~~~~~~~~~ + +If you're serving HTML, you'll want to render a template. The ``render()`` +method renders a template **and** puts that content into a ``Response`` +object for you:: + + // renders app/Resources/views/lucky/number.html.twig + return $this->render('lucky/number.html.twig', array('number' => $number)); + +Templates can also live in deeper sub-directories. Just try to avoid +creating unnecessarily deep structures:: + + // renders app/Resources/views/lottery/lucky/number.html.twig + return $this->render('lottery/lucky/number.html.twig', array( + 'number' => $number, + )); + +The Symfony templating system and Twig are explained more in the +:doc:`Creating and Using Templates article `. + +.. index:: + single: Controller; Accessing services + +.. _controller-accessing-services: + +Accessing Other Services +~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony comes packed with a lot of useful objects, called *services*. These +are used for rendering templates, sending emails, querying the database and +any other "work" you can think of. When you install a new bundle, it probably +brings in even *more* services. + +When extending the base controller class, you can access any Symfony service +via the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::get` +method of the ``Controller`` class. Here are several common services you might +need:: + + $templating = $this->get('templating'); + + $router = $this->get('router'); + + $mailer = $this->get('mailer'); + +What other services exist? To list all services, use the ``debug:container`` +console command: + +.. code-block:: terminal + + $ php app/console debug:container + +.. versionadded:: 2.6 + Prior to Symfony 2.6, this command was called ``container:debug``. + +For more information, see the :doc:`/service_container` article. + +.. tip:: + + To get a :ref:`container configuration parameter `, + use the + :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getParameter` + method:: + + $from = $this->getParameter('app.mailer.from'); + + .. versionadded:: 2.7 + The ``Controller::getParameter()`` method was introduced in Symfony + 2.7. Use ``$this->container->getParameter()`` in versions prior to 2.7. + +.. index:: + single: Controller; Managing errors + single: Controller; 404 pages + +Managing Errors and 404 Pages +----------------------------- + +When things are not found, you should play well with the HTTP protocol and +return a 404 response. To do this, you'll throw a special type of exception. +If you're extending the base ``Controller`` class, do the following:: + + public function indexAction() + { + // retrieve the object from database + $product = ...; + if (!$product) { + throw $this->createNotFoundException('The product does not exist'); + } + + return $this->render(...); + } + +The :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::createNotFoundException` +method is just a shortcut to create a special +:class:`Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException` +object, which ultimately triggers a 404 HTTP response inside Symfony. + +If you throw an exception that extends or is an instance of +:class:`Symfony\\Component\\HttpKernel\\Exception\\HttpException`, Symfony will +use the appropriate HTTP status code. Otherwise, the response will have a 500 +HTTP status code:: + + // this exception ultimately generates a 500 status error + throw new \Exception('Something went wrong!'); + +In every case, an error page is shown to the end user and a full debug +error page is shown to the developer (i.e. when you're using the ``app_dev.php`` +front controller - see :ref:`page-creation-environments`). + +You'll want to customize the error page your user sees. To do that, see +the :doc:`/controller/error_pages` article. + +.. index:: + single: Controller; The session + single: Session + +.. _controller-request-argument: + +The Request object as a Controller Argument +------------------------------------------- + +What if you need to read query parameters, grab a request header or get access +to an uploaded file? All of that information is stored in Symfony's ``Request`` +object. To get it in your controller, just add it as an argument and +**type-hint it with the Request class**:: + + use Symfony\Component\HttpFoundation\Request; + + public function indexAction(Request $request, $firstName, $lastName) + { + $page = $request->query->get('page', 1); + + // ... + } + +:ref:`Keep reading ` for more information about using the +Request object. + +Managing the Session +-------------------- + +Symfony provides a nice session object that you can use to store information +about the user between requests. By default, Symfony stores the token in a +cookie and writes the attributes to a file by using native PHP sessions. + +To retrieve the session, call +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getSession` +method on the ``Request`` object. This method returns a +:class:`Symfony\\Component\\HttpFoundation\\Session\\SessionInterface` with easy +methods for storing and fetching things from the session:: + + use Symfony\Component\HttpFoundation\Request; + + public function indexAction(Request $request) + { + $session = $request->getSession(); + + // stores an attribute for reuse during a later user request + $session->set('foo', 'bar'); + + // gets the attribute set by another controller in another request + $foobar = $session->get('foobar'); + + // uses a default value if the attribute doesn't exist + $filters = $session->get('filters', array()); + } + +Stored attributes remain in the session for the remainder of that user's session. + +.. index:: + single: Session; Flash messages + +Flash Messages +~~~~~~~~~~~~~~ + +You can also store special messages, called "flash" messages, on the user's +session. By design, flash messages are meant to be used exactly once: they vanish +from the session automatically as soon as you retrieve them. This feature makes +"flash" messages particularly great for storing user notifications. + +For example, imagine you're processing a :doc:`form ` submission:: + + use Symfony\Component\HttpFoundation\Request; + + public function updateAction(Request $request) + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + // do some sort of processing + + $this->addFlash( + 'notice', + 'Your changes were saved!' + ); + // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add() + + return $this->redirectToRoute(...); + } + + return $this->render(...); + } + +After processing the request, the controller sets a flash message in the session +and then redirects. The message key (``notice`` in this example) can be anything: +you'll use this key to retrieve the message. + +In the template of the next page (or even better, in your base layout template), +read any flash messages from the session: + +.. configuration-block:: + + .. code-block:: html+twig + + {# app/Resources/views/base.html.twig #} + + {# you can read and display just one flash message type... #} + {% for flash_message in app.session.flashBag.get('notice') %} +
+ {{ flash_message }} +
+ {% endfor %} + + {# ...or you can read and display every flash message available #} + {% for type, flash_messages in app.session.flashBag.all %} + {% for flash_message in flash_messages %} +
+ {{ flash_message }} +
+ {% endfor %} + {% endfor %} + + .. code-block:: html+php + + + + // you can read and display just one flash message type... + getFlashBag()->get('notice') as $message): ?> +
+ +
+ + + // ...or you can read and display every flash message available + getFlashBag()->all() as $type => $flash_messages): ?> + +
+ +
+ + + +.. note:: + + It's common to use ``notice``, ``warning`` and ``error`` as the keys of the + different types of flash messages, but you can use any key that fits your + needs. + +.. tip:: + + You can use the + :method:`Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface::peek` + method instead to retrieve the message while keeping it in the bag. + +.. index:: + single: Controller; Response object + +.. _request-object-info: + +The Request and Response Object +------------------------------- + +As mentioned :ref:`earlier `, the framework will +pass the ``Request`` object to any controller argument that is type-hinted with +the ``Request`` class:: + + use Symfony\Component\HttpFoundation\Request; + + public function indexAction(Request $request) + { + $request->isXmlHttpRequest(); // is it an Ajax request? + + $request->getPreferredLanguage(array('en', 'fr')); + + // retrieves GET and POST variables respectively + $request->query->get('page'); + $request->request->get('page'); + + // retrieves SERVER variables + $request->server->get('HTTP_HOST'); + + // retrieves an instance of UploadedFile identified by foo + $request->files->get('foo'); + + // retrieves a COOKIE value + $request->cookies->get('PHPSESSID'); + + // retrieves an HTTP request header, with normalized, lowercase keys + $request->headers->get('host'); + $request->headers->get('content_type'); + } + +The ``Request`` class has several public properties and methods that return any +information you need about the request. + +Like the ``Request``, the ``Response`` object has also a public ``headers`` property. +This is a :class:`Symfony\\Component\\HttpFoundation\\ResponseHeaderBag` that has +some nice methods for getting and setting response headers. The header names are +normalized so that using ``Content-Type`` is equivalent to ``content-type`` or even +``content_type``. + +The only requirement for a controller is to return a ``Response`` object. +The :class:`Symfony\\Component\\HttpFoundation\\Response` class is an +abstraction around the HTTP response - the text-based message filled with +headers and content that's sent back to the client:: + + use Symfony\Component\HttpFoundation\Response; + + // creates a simple Response with a 200 status code (the default) + $response = new Response('Hello '.$name, Response::HTTP_OK); + + // JsonResponse is a sub-class of Response + $response = new JsonResponse(array('name' => $name)); + // sets a header! + $response->headers->set('X-Rate-Limit', 10); + +There are special classes that make certain kinds of responses easier: + +* For JSON, there is :class:`Symfony\\Component\\HttpFoundation\\JsonResponse`. + See :ref:`component-http-foundation-json-response`. + +* For files, there is :class:`Symfony\\Component\\HttpFoundation\\BinaryFileResponse`. + See :ref:`component-http-foundation-serving-files`. + +* For streamed responses, there is + :class:`Symfony\\Component\\HttpFoundation\\StreamedResponse`. + See :ref:`streaming-response`. + +.. seealso:: + + Now that you know the basics you can continue your research on Symfony + ``Request`` and ``Response`` object in the + :ref:`HttpFoundation component documentation `. + +Final Thoughts +-------------- + +Whenever you create a page, you'll ultimately need to write some code that +contains the logic for that page. In Symfony, this is called a controller, +and it's a PHP function where you can do anything in order to return the +final ``Response`` object that will be returned to the user. + +To make life easier, you'll probably extend the base ``Controller`` class because +this gives two things: + +A) Shortcut methods (like ``render()`` and ``redirectToRoute()``); + +B) Access to *all* of the useful objects (services) in the system via the + :ref:`get() ` method. + +In other articles, you'll learn how to use specific services from inside your controller +that will help you persist and fetch objects from a database, process form submissions, +handle caching and more. + +Keep Going! +----------- + +Next, learn all about :doc:`rendering templates with Twig `. + +Learn more about Controllers +---------------------------- + +.. toctree:: + :hidden: + + templating + +.. toctree:: + :maxdepth: 1 + :glob: + + controller/* + +.. _`unvalidated redirects security vulnerability`: https://www.owasp.org/index.php/Open_redirect diff --git a/controller/csrf_token_validation.rst b/controller/csrf_token_validation.rst new file mode 100644 index 00000000000..5bf60980925 --- /dev/null +++ b/controller/csrf_token_validation.rst @@ -0,0 +1,28 @@ +.. index:: + single: Controller; Validating CSRF Tokens + +How to Manually Validate a CSRF Token in a Controller +===================================================== + +Sometimes, you want to use CSRF protection in an action where you do not +want to use the Symfony Form component. If, for example, you are implementing +a DELETE action, you can use the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::isCsrfTokenValid` +method to check the validity of a CSRF token:: + + public function deleteAction() + { + if ($this->isCsrfTokenValid('token_id', $submittedToken)) { + // ... do something, like deleting an object + } + } + +.. versionadded:: 2.6 + The ``isCsrfTokenValid()`` shortcut method was introduced in Symfony 2.6. + It is equivalent to executing the following code: + + .. code-block:: php + + use Symfony\Component\Security\Csrf\CsrfToken; + + $this->get('security.csrf.token_manager') + ->isTokenValid(new CsrfToken('token_id', 'TOKEN')); diff --git a/cookbook/controller/error_pages.rst b/controller/error_pages.rst similarity index 81% rename from cookbook/controller/error_pages.rst rename to controller/error_pages.rst index 1b4bc784dff..1777d5fea63 100644 --- a/cookbook/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -9,19 +9,23 @@ In Symfony applications, all errors are treated as exceptions, no matter if they are just a 404 Not Found error or a fatal error triggered by throwing some exception in your code. -In the :doc:`development environment `, +In the :doc:`development environment `, Symfony catches all the exceptions and displays a special **exception page** with lots of debug information to help you quickly discover the root problem: -.. image:: /images/cookbook/controller/error_pages/exceptions-in-dev-environment.png +.. image:: /_images/controller/error_pages/exceptions-in-dev-environment.png :alt: A typical exception page in the development environment + :align: center + :class: with-browser Since these pages contain a lot of sensitive internal information, Symfony won't display them in the production environment. Instead, it'll show a simple and generic **error page**: -.. image:: /images/cookbook/controller/error_pages/errors-in-prod-environment.png +.. image:: /_images/controller/error_pages/errors-in-prod-environment.png :alt: A typical error page in the production environment + :align: center + :class: with-browser Error pages for the production environment can be customized in different ways depending on your needs: @@ -44,7 +48,7 @@ Overriding the Default Error Templates When the error page loads, an internal :class:`Symfony\\Bundle\\TwigBundle\\Controller\\ExceptionController` is used to render a Twig template to show the user. -.. _cookbook-error-pages-by-status-code: +.. _controller-error-pages-by-status-code: This controller uses the HTTP status code, the request format and the following logic to determine the template filename: @@ -62,7 +66,7 @@ logic to determine the template filename: .. _overriding-or-adding-templates: To override these templates, simply rely on the standard Symfony method for -:ref:`overriding templates that live inside a bundle `: +:doc:`overriding templates that live inside a bundle `: put them in the ``app/Resources/TwigBundle/views/Exception/`` directory. A typical project that returns HTML and JSON pages, might look like this: @@ -87,7 +91,7 @@ Example 404 Error Template To override the 404 error template for HTML pages, create a new ``error404.html.twig`` template located at ``app/Resources/TwigBundle/views/Exception/``: -.. code-block:: html+jinja +.. code-block:: html+twig {# app/Resources/TwigBundle/views/Exception/error404.html.twig #} {% extends 'base.html.twig' %} @@ -95,11 +99,6 @@ To override the 404 error template for HTML pages, create a new {% block body %}

Page not found

- {# example security usage, see below #} - {% if app.user and is_granted('IS_AUTHENTICATED_FULLY') %} - {# ... #} - {% endif %} -

The requested page couldn't be located. Checkout for any URL misspelling or return to the homepage. @@ -136,12 +135,14 @@ The cause of this problem is that routing is done before security. If a 404 erro occurs, the security layer isn't loaded and thus, the ``is_granted()`` function is undefined. The solution is to add the following check before using this function: -.. code-block:: jinja +.. code-block:: twig {% if app.user and is_granted('...') %} {# ... #} {% endif %} +.. _testing-error-pages: + Testing Error Pages during Development ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -149,17 +150,7 @@ While you're in the development environment, Symfony shows the big *exception* page instead of your shiny new customized error page. So, how can you see what it looks like and debug it? -The recommended solution is to use a third-party bundle called `WebfactoryExceptionsBundle`_. -This bundle provides a special test controller that allows you to easily display -custom error pages for arbitrary HTTP status codes even when ``kernel.debug`` is -set to ``true``. - -.. _testing-error-pages: - -Testing Error Pages during Development -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The default ``ExceptionController`` also allows you to preview your +Fortunately, the default ``ExceptionController`` allows you to preview your *error* pages during development. .. versionadded:: 2.6 @@ -196,13 +187,13 @@ To use this feature, you need to have a definition in your // app/config/routing_dev.php use Symfony\Component\Routing\RouteCollection; - $collection = new RouteCollection(); - $collection->addCollection( + $routes = new RouteCollection(); + $routes->addCollection( $loader->import('@TwigBundle/Resources/config/routing/errors.xml') ); - $collection->addPrefix("/_error"); + $routes->addPrefix("/_error"); - return $collection; + return $routes; If you're coming from an older version of Symfony, you might need to add this to your ``routing_dev.yml`` file. If you're starting from @@ -238,7 +229,7 @@ configuration option to point to it: # app/config/config.yml twig: - exception_controller: AppBundle:Exception:showException + exception_controller: AppBundle:Exception:showException .. code-block:: xml @@ -255,6 +246,7 @@ configuration option to point to it: AppBundle:Exception:showException + .. code-block:: php @@ -283,6 +275,58 @@ also extend the default :class:`Symfony\\Bundle\\TwigBundle\\Controller\\Excepti In that case, you might want to override one or both of the ``showAction()`` and ``findTemplate()`` methods. The latter one locates the template to be used. +.. note:: + + In case of extending the + :class:`Symfony\\Bundle\\TwigBundle\\Controller\\ExceptionController` you + may configure a service to pass the Twig environment and the ``debug`` flag + to the constructor. + + .. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + app.exception_controller: + class: AppBundle\Controller\CustomExceptionController + arguments: ['@twig', '%kernel.debug%'] + + .. code-block:: xml + + + + + + + + + %kernel.debug% + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\Controller\CustomExceptionController; + use Symfony\Component\DependencyInjection\Reference; + + $container->register('app.exception_controller', CustomExceptionController::class) + ->setArguments(array( + new Reference('twig'), + '%kernel.debug%', + )); + + And then configure ``twig.exception_controller`` using the controller as + services syntax (e.g. ``app.exception_controller:showAction``). + .. tip:: The :ref:`error page preview ` also works for @@ -302,7 +346,7 @@ before, but also requires a thorough understanding of Symfony internals. Suppose that your code throws specialized exceptions with a particular meaning to your application domain. -:doc:`Writing your own event listener ` +:doc:`Writing your own event listener ` for the ``kernel.exception`` event allows you to have a closer look at the exception and take different actions depending on it. Those actions might include logging the exception, redirecting the user to another page or rendering specialized @@ -331,4 +375,3 @@ time and again, you can have just one (or several) listeners deal with them. .. _`WebfactoryExceptionsBundle`: https://github.com/webfactory/exceptions-bundle .. _`Symfony Standard Edition`: https://github.com/symfony/symfony-standard/ .. _`ExceptionListener`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php -.. _`development environment`: http://symfony.com/doc/current/cookbook/configuration/environments.html diff --git a/controller/forwarding.rst b/controller/forwarding.rst new file mode 100644 index 00000000000..df8a4c685b1 --- /dev/null +++ b/controller/forwarding.rst @@ -0,0 +1,35 @@ +.. index:: + single: Controller; Forwarding + +How to Forward Requests to another Controller +============================================= + +Though not very common, you can also forward to another controller internally +with the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::forward` +method. Instead of redirecting the user's browser, this makes an "internal" +sub-request and calls the defined controller. The ``forward()`` method returns +the :class:`Symfony\\Component\\HttpFoundation\\Response` object that is returned +from *that* controller:: + + public function indexAction($name) + { + $response = $this->forward('AppBundle:Something:fancy', array( + 'name' => $name, + 'color' => 'green', + )); + + // ... further modify the response or return it directly + + return $response; + } + +The array passed to the method becomes the arguments for the resulting controller. +The target controller method might look something like this:: + + public function fancyAction($name, $color) + { + // ... create and return a Response object + } + +Just like when creating a controller for a route, the order of the arguments +of ``fancyAction()`` doesn't matter: the matching is done by name. diff --git a/cookbook/controller/service.rst b/controller/service.rst similarity index 60% rename from cookbook/controller/service.rst rename to controller/service.rst index 94e4202f2a1..0a8385bd0cd 100644 --- a/cookbook/controller/service.rst +++ b/controller/service.rst @@ -4,31 +4,42 @@ How to Define Controllers as Services ===================================== -In the book, you've learned how easily a controller can be used when it -extends the base -:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class. While -this works fine, controllers can also be specified as services. +.. caution:: -.. note:: + Defining controllers as services is **not officially recommended** by Symfony. + They are used by some developers for very specific use cases, such as + DDD (*domain-driven design*) and Hexagonal Architecture applications. - Specifying a controller as a service takes a bit more work. The - primary advantage is that the entire controller or any services passed to - the controller can be modified via the service container configuration. - This is especially useful when developing an open-source bundle or any - bundle that will be used in different projects. - - A second advantage is that your controllers are more "sandboxed". By - looking at the constructor arguments, it's easy to see what types of things - this controller may or may not do. And because each dependency needs - to be injected manually, it's more obvious (i.e. if you have many constructor - arguments) when your controller is becoming too big. The recommendation from - the :doc:`best practices ` is also valid for - controllers defined as services: Avoid putting your business logic into the - controllers. Instead, inject services that do the bulk of the work. - - So, even if you don't specify your controllers as services, you'll likely - see this done in some open-source Symfony bundles. It's also important - to understand the pros and cons of both approaches. +In the :doc:`/controller` guide, you've learned how easily a controller can be +used when it extends the base +:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class. While +this works fine, controllers can also be specified as services. Even if you don't +specify your controllers as services, you might see them being used in some +open-source Symfony bundles, so it may be useful to understand both approaches. + +These are the main **advantages** of defining controllers as services: + +* The entire controller and any service passed to it can be modified via the + service container configuration. This is useful when developing reusable bundles; +* Your controllers are more "sandboxed". By looking at the constructor arguments, + it's easy to see what types of things this controller may or may not do; +* If you're not passing some required dependencies or if you are injecting some + non-existent services, you'll get errors during the container compilation + instead of during runtime execution; +* Since dependencies must be injected manually, it's more obvious when your + controller is becoming too big (i.e. if you have many constructor arguments). + +These are the main **drawbacks** of defining controllers as services: + +* It takes more work to create the controllers and they become more verbose + because they don't have automatic access to the services and the base + controller shortcuts; +* The constructor of the controllers can rapidly become too complex because you + must inject every single dependency needed by them. + +The recommendation from the :doc:`best practices ` +is also valid for controllers defined as services: avoid putting your business +logic into the controllers. Instead, inject services that do the bulk of the work. Defining the Controller as a Service ------------------------------------ @@ -63,18 +74,24 @@ Then you can define it as a service as follows: .. code-block:: xml - - - + + + + + + + + .. code-block:: php // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; + use AppBundle\Controller\HelloController; - $container->setDefinition('app.hello_controller', new Definition( - 'AppBundle\Controller\HelloController' - )); + $container->register('app.hello_controller', HelloController::class); Referring to the Service ------------------------ @@ -87,8 +104,9 @@ defined above with the id ``app.hello_controller``:: .. note:: - You cannot drop the ``Action`` part of the method name when using this - syntax. + Make sure the method name in your route (e.g. ``indexAction``) matches the + method name exactly. Unlike the traditional ``Bundle:Controller:method`` + notation, the ``Action`` suffix is not automatically added for you. You can also route to the service by using the same notation when defining the route ``_controller`` value: @@ -105,9 +123,17 @@ the route ``_controller`` value: .. code-block:: xml - - app.hello_controller:indexAction - + + + + + app.hello_controller:indexAction + + + .. code-block:: php @@ -119,12 +145,17 @@ the route ``_controller`` value: .. tip:: You can also use annotations to configure routing using a controller - defined as a service. See the `FrameworkExtraBundle documentation`_ for + defined as a service. Make sure you specify the service ID in the + ``@Route`` annotation. See the `FrameworkExtraBundle documentation`_ for details. -.. versionadded:: 2.6 - If your controller service implements the ``__invoke`` method, you can simply refer to the service id - (``app.hello_controller``). +.. tip:: + + If your controller implements the ``__invoke()`` method, you can simply + refer to the service id (``app.hello_controller``). + + .. versionadded:: 2.6 + Support for ``__invoke()`` was introduced in Symfony 2.6. Alternatives to base Controller Methods --------------------------------------- @@ -149,13 +180,13 @@ Symfony's base controller:: public function indexAction($name) { return $this->render( - 'AppBundle:Hello:index.html.twig', + 'hello/index.html.twig', array('name' => $name) ); } } -If you look at the source code for the ``render`` function in Symfony's +If you look at the source code for the ``render()`` function in Symfony's `base Controller class`_, you'll see that this method actually uses the ``templating`` service:: @@ -185,7 +216,7 @@ service and use it directly:: public function indexAction($name) { return $this->templating->renderResponse( - 'AppBundle:Hello:index.html.twig', + 'hello/index.html.twig', array('name' => $name) ); } @@ -202,41 +233,47 @@ argument: services: app.hello_controller: class: AppBundle\Controller\HelloController - arguments: ["@templating"] + arguments: ['@templating'] .. code-block:: xml - - - - - + + + + + + + + + + .. code-block:: php // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; + use AppBundle\Controller\HelloController; use Symfony\Component\DependencyInjection\Reference; - $container->setDefinition('app.hello_controller', new Definition( - 'AppBundle\Controller\HelloController', - array(new Reference('templating')) - )); + $container->register('app.hello_controller', HelloController::class) + ->addArgument(new Reference('templating')); Rather than fetching the ``templating`` service from the container, you can inject *only* the exact service(s) that you need directly into the controller. .. note:: - This does not mean that you cannot extend these controllers from your own - base controller. The move away from the standard base controller is because - its helper methods rely on having the container available which is not - the case for controllers that are defined as services. It may be a good - idea to extract common code into a service that's injected rather than - place that code into a base controller that you extend. Both approaches - are valid, exactly how you want to organize your reusable code is up to - you. + This does not mean that you cannot extend these controllers from your own + base controller. The move away from the standard base controller is because + its helper methods rely on having the container available which is not + the case for controllers that are defined as services. It may be a good + idea to extract common code into a service that's injected rather than + place that code into a base controller that you extend. Both approaches + are valid, exactly how you want to organize your reusable code is up to + you. Base Controller Methods and Their Service Replacements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -273,11 +310,15 @@ controller: :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::generateUrl` (service: ``router``) .. code-block:: php - $router->generate($route, $params, $absolute); + $router->generate($route, $params, $referenceType); -:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getDoctrine` (service: ``doctrine``) + .. note:: - *Simply inject doctrine instead of fetching it from the container* + The ``$referenceType`` argument must be one of the constants defined + in the :class:`Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface`. + +:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getDoctrine` (service: ``doctrine``) + *Simply inject doctrine instead of fetching it from the container.* :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getUser` (service: ``security.token_storage``) .. code-block:: php @@ -318,17 +359,16 @@ controller: $templating = $this->templating; $callback = function () use ($templating, $view, $parameters) { $templating->stream($view, $parameters); - } + }; return new StreamedResponse($callback); .. tip:: - ``getRequest`` has been deprecated. Instead, have an argument to your + ``getRequest()`` has been deprecated. Instead, have an argument to your controller action method called ``Request $request``. The order of the parameters is not important, but the typehint must be provided. - .. _`Controller class source code`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php .. _`base Controller class`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php -.. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html +.. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html#controller-as-service diff --git a/cookbook/web_services/php_soap_extension.rst b/controller/soap_web_service.rst similarity index 79% rename from cookbook/web_services/php_soap_extension.rst rename to controller/soap_web_service.rst index 98edaea09b9..00ae10c083f 100644 --- a/cookbook/web_services/php_soap_extension.rst +++ b/controller/soap_web_service.rst @@ -8,7 +8,7 @@ How to Create a SOAP Web Service in a Symfony Controller Setting up a controller to act as a SOAP server is simple with a couple tools. You must, of course, have the `PHP SOAP`_ extension installed. -As the PHP SOAP extension can not currently generate a WSDL, you must either +As the PHP SOAP extension cannot currently generate a WSDL, you must either create one from scratch or use a 3rd party generator. .. note:: @@ -39,10 +39,9 @@ In this case, the SOAP service will allow the client to call a method called public function hello($name) { - $message = \Swift_Message::newInstance() - ->setTo('me@example.com') - ->setSubject('Hello Service') - ->setBody($name . ' says hi!'); + $message = new \Swift_Message('Hello Service') + ->setTo('me@example.com') + ->setBody($name.' says hi!'); $this->mailer->send($message); @@ -63,29 +62,37 @@ a ``HelloService`` object properly: services: hello_service: class: Acme\SoapBundle\Services\HelloService - arguments: ["@mailer"] + arguments: ['@mailer'] .. code-block:: xml - - - - - + + + + + + + + + + .. code-block:: php // app/config/services.php + use Acme\SoapBundle\Services\HelloService; + $container - ->register('hello_service', 'Acme\SoapBundle\Services\HelloService') + ->register('hello_service', HelloService::class) ->addArgument(new Reference('mailer')); Below is an example of a controller that is capable of handling a SOAP request. If ``indexAction()`` is accessible via the route ``/soap``, then the -WSDL document can be retrieved via ``/soap?wsdl``. - -.. code-block:: php +WSDL document can be retrieved via ``/soap?wsdl``:: namespace Acme\SoapBundle\Controller; @@ -96,14 +103,14 @@ WSDL document can be retrieved via ``/soap?wsdl``. { public function indexAction() { - $server = new \SoapServer('/path/to/hello.wsdl'); - $server->setObject($this->get('hello_service')); + $soapServer = new \SoapServer('/path/to/hello.wsdl'); + $soapServer->setObject($this->get('hello_service')); $response = new Response(); $response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1'); ob_start(); - $server->handle(); + $soapServer->handle(); $response->setContent(ob_get_clean()); return $response; @@ -121,12 +128,12 @@ into the content of the Response and clear the output buffer. Finally, you're ready to return the ``Response``. Below is an example calling the service using a `NuSOAP`_ client. This example -assumes that the ``indexAction`` in the controller above is accessible via the +assumes that the ``indexAction()`` in the controller above is accessible via the route ``/soap``:: - $client = new \Soapclient('http://example.com/app.php/soap?wsdl', true); + $soapClient = new \Soapclient('http://example.com/app.php/soap?wsdl'); - $result = $client->call('hello', array('name' => 'Scott')); + $result = $soapClient->call('hello', array('name' => 'Scott')); An example WSDL is below. @@ -190,7 +197,7 @@ An example WSDL is below. -.. _`PHP SOAP`: http://php.net/manual/en/book.soap.php +.. _`PHP SOAP`: https://php.net/manual/en/book.soap.php .. _`NuSOAP`: http://sourceforge.net/projects/nusoap -.. _`output buffering`: http://php.net/manual/en/book.outcontrol.php +.. _`output buffering`: https://php.net/manual/en/book.outcontrol.php .. _`Zend SOAP`: http://framework.zend.com/manual/current/en/modules/zend.soap.server.html diff --git a/controller/upload_file.rst b/controller/upload_file.rst new file mode 100644 index 00000000000..3409ddee448 --- /dev/null +++ b/controller/upload_file.rst @@ -0,0 +1,469 @@ +.. index:: + single: Controller; Upload; File + +How to Upload Files +=================== + +.. note:: + + Instead of handling file uploading yourself, you may consider using the + `VichUploaderBundle`_ community bundle. This bundle provides all the common + operations (such as file renaming, saving and deleting) and it's tightly + integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel. + +Imagine that you have a ``Product`` entity in your application and you want to +add a PDF brochure for each product. To do so, add a new property called ``brochure`` +in the ``Product`` entity:: + + // src/AppBundle/Entity/Product.php + namespace AppBundle\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + class Product + { + // ... + + /** + * @ORM\Column(type="string") + * + * @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.") + * @Assert\File(mimeTypes={ "application/pdf" }) + */ + private $brochure; + + public function getBrochure() + { + return $this->brochure; + } + + public function setBrochure($brochure) + { + $this->brochure = $brochure; + + return $this; + } + } + +Note that the type of the ``brochure`` column is ``string`` instead of ``binary`` +or ``blob`` because it just stores the PDF file name instead of the file contents. + +Then, add a new ``brochure`` field to the form that manages the ``Product`` entity:: + + // src/AppBundle/Form/ProductType.php + namespace AppBundle\Form; + + use AppBundle\Entity\Product; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class ProductType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('brochure', 'file', array('label' => 'Brochure (PDF file)')) + // ... + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => Product::class, + )); + } + + public function getName() + { + return 'product'; + } + } + +Now, update the template that renders the form to display the new ``brochure`` +field (the exact template code to add depends on the method used by your application +to :doc:`customize form rendering `): + +.. configuration-block:: + + .. code-block:: html+twig + + {# app/Resources/views/product/new.html.twig #} +

Adding a new product

+ + {{ form_start(form) }} + {# ... #} + + {{ form_row(form.brochure) }} + {{ form_end(form) }} + + .. code-block:: html+php + + +

Adding a new product

+ + start($form) ?> + row($form['brochure']) ?> + end($form) ?> + +Finally, you need to update the code of the controller that handles the form:: + + // src/AppBundle/Controller/ProductController.php + namespace AppBundle\Controller; + + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\HttpFoundation\Request; + use AppBundle\Entity\Product; + use AppBundle\Form\ProductType; + + class ProductController extends Controller + { + /** + * @Route("/product/new", name="app_product_new") + */ + public function newAction(Request $request) + { + $product = new Product(); + $form = $this->createForm(new ProductType(), $product); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // $file stores the uploaded PDF file + /** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */ + $file = $product->getBrochure(); + + $fileName = $this->generateUniqueFileName().'.'.$file->guessExtension(); + + // moves the file to the directory where brochures are stored + $file->move( + $this->getParameter('brochures_directory'), + $fileName + ); + + // updates the 'brochure' property to store the PDF file name + // instead of its contents + $product->setBrochure($fileName); + + // ... persist the $product variable or any other work + + return $this->redirect($this->generateUrl('app_product_list')); + } + + return $this->render('product/new.html.twig', array( + 'form' => $form->createView(), + )); + } + + /** + * @return string + */ + private function generateUniqueFileName() + { + // md5() reduces the similarity of the file names generated by + // uniqid(), which is based on timestamps + return md5(uniqid()); + } + } + +Now, create the ``brochures_directory`` parameter that was used in the +controller to specify the directory in which the brochures should be stored: + +.. code-block:: yaml + + # app/config/config.yml + + # ... + parameters: + brochures_directory: '%kernel.root_dir%/../web/uploads/brochures' + +There are some important things to consider in the code of the above controller: + +#. When the form is uploaded, the ``brochure`` property contains the whole PDF + file contents. Since this property stores just the file name, you must set + its new value before persisting the changes of the entity; +#. In Symfony applications, uploaded files are objects of the + :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class. This class + provides methods for the most common operations when dealing with uploaded files; +#. A well-known security best practice is to never trust the input provided by + users. This also applies to the files uploaded by your visitors. The ``UploadedFile`` + class provides methods to get the original file extension + (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getExtension`), + the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientSize`) + and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`). + However, they are considered *not safe* because a malicious user could tamper + that information. That's why it's always better to generate a unique name and + use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension` + method to let Symfony guess the right extension according to the file MIME type; + +You can use the following code to link to the PDF brochure of a product: + +.. configuration-block:: + + .. code-block:: html+twig + + View brochure (PDF) + + .. code-block:: html+php + + + View brochure (PDF) + + +.. tip:: + + When creating a form to edit an already persisted item, the file form type + still expects a :class:`Symfony\\Component\\HttpFoundation\\File\\File` + instance. As the persisted entity now contains only the relative file path, + you first have to concatenate the configured upload path with the stored + filename and create a new ``File`` class:: + + use Symfony\Component\HttpFoundation\File\File; + // ... + + $product->setBrochure( + new File($this->getParameter('brochures_directory').'/'.$product->getBrochure()) + ); + +Creating an Uploader Service +---------------------------- + +To avoid logic in controllers, making them big, you can extract the upload +logic to a separate service:: + + // src/AppBundle/FileUploader.php + namespace AppBundle; + + use Symfony\Component\HttpFoundation\File\UploadedFile; + + class FileUploader + { + private $targetDirectory; + + public function __construct($targetDirectory) + { + $this->targetDirectory = $targetDirectory; + } + + public function upload(UploadedFile $file) + { + $fileName = md5(uniqid()).'.'.$file->guessExtension(); + + $file->move($this->getTargetDirectory(), $fileName); + + return $fileName; + } + + public function getTargetDirectory() + { + return $this->targetDirectory; + } + } + +Then, define a service for this class: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + # ... + app.brochure_uploader: + class: AppBundle\FileUploader + arguments: ['%brochures_directory%'] + + .. code-block:: xml + + + + + + + + %brochures_directory% + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\FileUploader; + + // ... + $container->register('app.brochure_uploader', FileUploader::class) + ->addArgument('%brochures_directory%'); + +Now you're ready to use this service in the controller:: + + // src/AppBundle/Controller/ProductController.php + + // ... + public function newAction(Request $request) + { + // ... + + if ($form->isSubmitted() && $form->isValid()) { + $file = $product->getBrochure(); + $fileName = $this->get('app.brochure_uploader')->upload($file); + + $product->setBrochure($fileName); + + // ... + } + + // ... + } + +Using a Doctrine Listener +------------------------- + +If you are using Doctrine to store the Product entity, you can create a +:doc:`Doctrine listener ` to +automatically upload the file when persisting the entity:: + + // src/AppBundle/EventListener/BrochureUploadListener.php + namespace AppBundle\EventListener; + + use Symfony\Component\HttpFoundation\File\UploadedFile; + use Doctrine\ORM\Event\LifecycleEventArgs; + use Doctrine\ORM\Event\PreUpdateEventArgs; + use AppBundle\Entity\Product; + use AppBundle\FileUploader; + + class BrochureUploadListener + { + private $uploader; + + public function __construct(FileUploader $uploader) + { + $this->uploader = $uploader; + } + + public function prePersist(LifecycleEventArgs $args) + { + $entity = $args->getEntity(); + + $this->uploadFile($entity); + } + + public function preUpdate(PreUpdateEventArgs $args) + { + $entity = $args->getEntity(); + + $this->uploadFile($entity); + } + + private function uploadFile($entity) + { + // upload only works for Product entities + if (!$entity instanceof Product) { + return; + } + + $file = $entity->getBrochure(); + + // only upload new files + if ($file instanceof UploadedFile) { + $fileName = $this->uploader->upload($file); + $entity->setBrochure($fileName); + } + } + } + +Now, register this class as a Doctrine listener: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + # ... + app.doctrine_brochure_listener: + class: AppBundle\EventListener\BrochureUploadListener + arguments: ['@app.brochure_uploader'] + tags: + - { name: doctrine.event_listener, event: prePersist } + - { name: doctrine.event_listener, event: preUpdate } + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\EventListener\BrochureUploaderListener; + use Symfony\Component\DependencyInjection\Reference; + + // ... + $container->register('app.doctrine_brochure_listener', BrochureUploaderListener::class) + ->addArgument(new Reference('app.brochure_uploader')) + ->addTag('doctrine.event_listener', array( + 'event' => 'prePersist', + )) + ->addTag('doctrine.event_listener', array( + 'event' => 'prePersist', + )); + +This listener is now automatically executed when persisting a new Product +entity. This way, you can remove everything related to uploading from the +controller. + +.. tip:: + + This listener can also create the ``File`` instance based on the path when + fetching entities from the database:: + + // ... + use Symfony\Component\HttpFoundation\File\File; + + // ... + class BrochureUploadListener + { + // ... + + public function postLoad(LifecycleEventArgs $args) + { + $entity = $args->getEntity(); + + if (!$entity instanceof Product) { + return; + } + + if ($fileName = $entity->getBrochure()) { + $entity->setBrochure(new File($this->uploader->getTargetDir().'/'.$fileName)); + } + } + } + + After adding these lines, configure the listener to also listen for the + ``postLoad`` event. + +.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle diff --git a/cookbook/assetic/index.rst b/cookbook/assetic/index.rst deleted file mode 100644 index c37efdc16ee..00000000000 --- a/cookbook/assetic/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Assetic -======= - -.. toctree:: - :maxdepth: 2 - - asset_management - php - uglifyjs - yuicompressor - jpeg_optimize - apply_to_option diff --git a/cookbook/cache/index.rst b/cookbook/cache/index.rst deleted file mode 100644 index 00dcfda66b6..00000000000 --- a/cookbook/cache/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Cache -===== - -.. toctree:: - :maxdepth: 2 - - varnish - form_csrf_caching diff --git a/cookbook/composer.rst b/cookbook/composer.rst deleted file mode 100644 index 212fa78f632..00000000000 --- a/cookbook/composer.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. index:: - double: Composer; Installation - -Installing Composer -=================== - -`Composer`_ is the package manager used by modern PHP applications. Use Composer -to manage dependencies in your Symfony applications and to install Symfony Components -in your PHP projects. - -It's recommended to install Composer globally in your system as explained in the -following sections. - -Install Composer on Linux and Mac OS X --------------------------------------- - -To install Composer on Linux or Mac OS X, execute the following two commands: - -.. code-block:: bash - - $ curl -sS https://getcomposer.org/installer | php - $ sudo mv composer.phar /usr/local/bin/composer - -.. note:: - - If you don't have ``curl`` installed, you can also just download the - ``installer`` file manually at https://getcomposer.org/installer and - then run: - - .. code-block:: bash - - $ php installer - $ sudo mv composer.phar /usr/local/bin/composer - -Install Composer on Windows ---------------------------- - -Download the installer from `getcomposer.org/download`_, execute it and follow -the instructions. - -Learn more ----------- - -Read the `Composer documentation`_ to learn more about its usage and features. - -.. _`Composer`: https://getcomposer.org/ -.. _`getcomposer.org/download`: https://getcomposer.org/download -.. _`Composer documentation`: https://getcomposer.org/doc/00-intro.md diff --git a/cookbook/configuration/index.rst b/cookbook/configuration/index.rst deleted file mode 100644 index 8bac5cf43be..00000000000 --- a/cookbook/configuration/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -Configuration -============= - -.. toctree:: - :maxdepth: 2 - - environments - override_dir_structure - using_parameters_in_dic - front_controllers_and_kernel - external_parameters - pdo_session_storage - apache_router - web_server_configuration - configuration_organization - mongodb_session_storage \ No newline at end of file diff --git a/cookbook/console/console_command.rst b/cookbook/console/console_command.rst deleted file mode 100644 index 184c51e541d..00000000000 --- a/cookbook/console/console_command.rst +++ /dev/null @@ -1,241 +0,0 @@ -.. index:: - single: Console; Create commands - -How to Create a Console Command -=============================== - -The Console page of the Components section (:doc:`/components/console/introduction`) covers -how to create a console command. This cookbook article covers the differences -when creating console commands within the Symfony Framework. - -Automatically Registering Commands ----------------------------------- - -To make the console commands available automatically with Symfony, create a -``Command`` directory inside your bundle and create a PHP file suffixed with -``Command.php`` for each command that you want to provide. For example, if you -want to extend the AppBundle to greet you from the command line, create -``GreetCommand.php`` and add the following to it:: - - // src/AppBundle/Command/GreetCommand.php - namespace AppBundle\Command; - - use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; - use Symfony\Component\Console\Input\InputArgument; - use Symfony\Component\Console\Input\InputInterface; - use Symfony\Component\Console\Input\InputOption; - use Symfony\Component\Console\Output\OutputInterface; - - class GreetCommand extends ContainerAwareCommand - { - protected function configure() - { - $this - ->setName('demo:greet') - ->setDescription('Greet someone') - ->addArgument( - 'name', - InputArgument::OPTIONAL, - 'Who do you want to greet?' - ) - ->addOption( - 'yell', - null, - InputOption::VALUE_NONE, - 'If set, the task will yell in uppercase letters' - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $name = $input->getArgument('name'); - if ($name) { - $text = 'Hello '.$name; - } else { - $text = 'Hello'; - } - - if ($input->getOption('yell')) { - $text = strtoupper($text); - } - - $output->writeln($text); - } - } - -This command will now automatically be available to run: - -.. code-block:: bash - - $ php app/console demo:greet Fabien - -.. _cookbook-console-dic: - -Register Commands in the Service Container -------------------------------------------- - -Just like controllers, commands can be declared as services. See the -:doc:`dedicated cookbook entry ` -for details. - -Getting Services from the Service Container -------------------------------------------- - -By using :class:`Symfony\\Bundle\\FrameworkBundle\\Command\\ContainerAwareCommand` -as the base class for the command (instead of the more basic -:class:`Symfony\\Component\\Console\\Command\\Command`), you have access to the -service container. In other words, you have access to any configured service:: - - protected function execute(InputInterface $input, OutputInterface $output) - { - $name = $input->getArgument('name'); - $logger = $this->getContainer()->get('logger'); - - $logger->info('Executing command for '.$name); - // ... - } - -However, due to the :doc:`container scopes ` this -code doesn't work for some services. For instance, if you try to get the ``request`` -service or any other service related to it, you'll get the following error: - -.. code-block:: text - - You cannot create a service ("request") of an inactive scope ("request"). - -Consider the following example that uses the ``translator`` service to -translate some contents using a console command:: - - protected function execute(InputInterface $input, OutputInterface $output) - { - $name = $input->getArgument('name'); - $translator = $this->getContainer()->get('translator'); - if ($name) { - $output->writeln( - $translator->trans('Hello %name%!', array('%name%' => $name)) - ); - } else { - $output->writeln($translator->trans('Hello!')); - } - } - -If you dig into the Translator component classes, you'll see that the ``request`` -service is required to get the locale into which the contents are translated:: - - // vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php - public function getLocale() - { - if (null === $this->locale && $this->container->isScopeActive('request') - && $this->container->has('request')) { - $this->locale = $this->container->get('request')->getLocale(); - } - - return $this->locale; - } - -Therefore, when using the ``translator`` service inside a command, you'll get the -previous *"You cannot create a service of an inactive scope"* error message. -The solution in this case is as easy as setting the locale value explicitly -before translating contents:: - - protected function execute(InputInterface $input, OutputInterface $output) - { - $name = $input->getArgument('name'); - $locale = $input->getArgument('locale'); - - $translator = $this->getContainer()->get('translator'); - $translator->setLocale($locale); - - if ($name) { - $output->writeln( - $translator->trans('Hello %name%!', array('%name%' => $name)) - ); - } else { - $output->writeln($translator->trans('Hello!')); - } - } - -However, for other services the solution might be more complex. For more details, -see :doc:`/cookbook/service_container/scopes`. - -Invoking other Commands ------------------------ - -See :ref:`calling-existing-command` if you need to implement a command that runs -other dependent commands. - -Testing Commands ----------------- - -When testing commands used as part of the full-stack framework, -:class:`Symfony\\Bundle\\FrameworkBundle\\Console\\Application ` -should be used instead of -:class:`Symfony\\Component\\Console\\Application `:: - - use Symfony\Component\Console\Tester\CommandTester; - use Symfony\Bundle\FrameworkBundle\Console\Application; - use AppBundle\Command\GreetCommand; - - class ListCommandTest extends \PHPUnit_Framework_TestCase - { - public function testExecute() - { - // mock the Kernel or create one depending on your needs - $application = new Application($kernel); - $application->add(new GreetCommand()); - - $command = $application->find('demo:greet'); - $commandTester = new CommandTester($command); - $commandTester->execute( - array( - 'name' => 'Fabien', - '--yell' => true, - ) - ); - - $this->assertRegExp('/.../', $commandTester->getDisplay()); - - // ... - } - } - -.. note:: - - In the specific case above, the ``name`` parameter and the ``--yell`` option - are not mandatory for the command to work, but are shown so you can see - how to customize them when calling the command. - -To be able to use the fully set up service container for your console tests -you can extend your test from -:class:`Symfony\\Bundle\\FrameworkBundle\\Test\\KernelTestCase`:: - - use Symfony\Component\Console\Tester\CommandTester; - use Symfony\Bundle\FrameworkBundle\Console\Application; - use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; - use AppBundle\Command\GreetCommand; - - class ListCommandTest extends KernelTestCase - { - public function testExecute() - { - $kernel = $this->createKernel(); - $kernel->boot(); - - $application = new Application($kernel); - $application->add(new GreetCommand()); - - $command = $application->find('demo:greet'); - $commandTester = new CommandTester($command); - $commandTester->execute( - array( - 'name' => 'Fabien', - '--yell' => true, - ) - ); - - $this->assertRegExp('/.../', $commandTester->getDisplay()); - - // ... - } - } diff --git a/cookbook/console/index.rst b/cookbook/console/index.rst deleted file mode 100644 index c3628beb4f7..00000000000 --- a/cookbook/console/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Console -======= - -.. toctree:: - :maxdepth: 2 - - console_command - usage - command_in_controller - sending_emails - logging - commands_as_services diff --git a/cookbook/controller/index.rst b/cookbook/controller/index.rst deleted file mode 100644 index 9ee8ca56a17..00000000000 --- a/cookbook/controller/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Controller -========== - -.. toctree:: - :maxdepth: 2 - - error_pages - service - upload_file diff --git a/cookbook/controller/upload_file.rst b/cookbook/controller/upload_file.rst deleted file mode 100644 index dccd5676773..00000000000 --- a/cookbook/controller/upload_file.rst +++ /dev/null @@ -1,177 +0,0 @@ -.. index:: - single: Controller; Upload; File - -How to Upload Files -=================== - -.. note:: - - Instead of handling file uploading yourself, you may consider using the - `VichUploaderBundle`_ community bundle. This bundle provides all the common - operations (such as file renaming, saving and deleting) and it's tightly - integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel. - -Imagine that you have a ``Product`` entity in your application and you want to -add a PDF brochure for each product. To do so, add a new property called ``brochure`` -in the ``Product`` entity:: - - // src/AppBundle/Entity/Product.php - namespace AppBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Validator\Constraints as Assert; - - class Product - { - // ... - - /** - * @ORM\Column(type="string") - * - * @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.") - * @Assert\File(mimeTypes={ "application/pdf" }) - */ - private $brochure; - - public function getBrochure() - { - return $this->brochure; - } - - public function setBrochure($brochure) - { - $this->brochure = $brochure; - - return $this; - } - } - -Note that the type of the ``brochure`` column is ``string`` instead of ``binary`` -or ``blob`` because it just stores the PDF file name instead of the file contents. - -Then, add a new ``brochure`` field to the form that manage the ``Product`` entity:: - - // src/AppBundle/Form/ProductType.php - namespace AppBundle\Form; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - class ProductType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - // ... - ->add('brochure', 'file', array('label' => 'Brochure (PDF file)')) - // ... - ; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'AppBundle\Entity\Product', - )); - } - - public function getName() - { - return 'product'; - } - } - -Now, update the template that renders the form to display the new ``brochure`` -field (the exact template code to add depends on the method used by your application -to :doc:`customize form rendering `): - -.. code-block:: html+jinja - - {# app/Resources/views/product/new.html.twig #} -

Adding a new product

- - {{ form_start() }} - {# ... #} - - {{ form_row(form.brochure) }} - {{ form_end() }} - -Finally, you need to update the code of the controller that handles the form:: - - // src/AppBundle/Controller/ProductController.php - namespace AppBundle\ProductController; - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Request; - use AppBundle\Entity\Product; - use AppBundle\Form\ProductType; - - class ProductController extends Controller - { - /** - * @Route("/product/new", name="app_product_new") - */ - public function newAction(Request $request) - { - $product = new Product(); - $form = $this->createForm(new ProductType(), $product); - $form->handleRequest($request); - - if ($form->isValid()) { - // $file stores the uploaded PDF file - /** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */ - $file = $product->getBrochure() - - // Generate a unique name for the file before saving it - $fileName = md5(uniqid()).'.'.$file->guessExtension(); - - // Move the file to the directory where brochures are stored - $brochuresDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures'; - $file->move($brochuresDir, $fileName); - - // Update the 'brochure' property to store the PDF file name - // instead of its contents - $product->setBrochure($filename); - - // persist the $product variable or any other work... - - return $this->redirect($this->generateUrl('app_product_list')); - } - - return $this->render('product/new.html.twig', array( - 'form' => $form->createView() - )); - } - } - -There are some important things to consider in the code of the above controller: - -#. When the form is uploaded, the ``brochure`` property contains the whole PDF - file contents. Since this property stores just the file name, you must set - its new value before persisting the changes of the entity. -#. In Symfony applications, uploaded files are objects of the - :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class, which - provides methods for the most common operations when dealing with uploaded files. -#. A well-known security best practice is to never trust the input provided by - users. This also applies to the files uploaded by your visitors. The ``Uploaded`` - class provides methods to get the original file extension (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getExtension`), - the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize`) - and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName`). - However, they are considered *not safe* because a malicious user could tamper - that information. That's why it's always better to generate a unique name and - use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension` - method to let Symfony guess the right extension according to the file MIME type. -#. The ``UploadedFile`` class also provides a :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::move` - method to store the file in its intended directory. Defining this directory - path as an application configuration option is considered a good practice that - simplifies the code: ``$this->container->getParameter('brochures_dir')``. - -You can now use the following code to link to the PDF brochure of an product: - -.. code-block:: html+jinja - - View brochure (PDF) - -.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle diff --git a/cookbook/deployment/index.rst b/cookbook/deployment/index.rst deleted file mode 100644 index 2b1a962fb54..00000000000 --- a/cookbook/deployment/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Deployment -========== - -.. toctree:: - :maxdepth: 2 - - tools - azure-website - heroku - platformsh diff --git a/cookbook/doctrine/custom_dql_functions.rst b/cookbook/doctrine/custom_dql_functions.rst deleted file mode 100644 index 747a353e7a5..00000000000 --- a/cookbook/doctrine/custom_dql_functions.rst +++ /dev/null @@ -1,72 +0,0 @@ -.. index:: - single: Doctrine; Custom DQL functions - -How to Register custom DQL Functions -==================================== - -Doctrine allows you to specify custom DQL functions. For more information -on this topic, read Doctrine's cookbook article "`DQL User Defined Functions`_". - -In Symfony, you can register your custom DQL functions as follows: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - doctrine: - orm: - # ... - dql: - string_functions: - test_string: AppBundle\DQL\StringFunction - second_string: AppBundle\DQL\SecondStringFunction - numeric_functions: - test_numeric: AppBundle\DQL\NumericFunction - datetime_functions: - test_datetime: AppBundle\DQL\DatetimeFunction - - .. code-block:: xml - - - - - - - - - AppBundle\DQL\StringFunction - AppBundle\DQL\SecondStringFunction - AppBundle\DQL\NumericFunction - AppBundle\DQL\DatetimeFunction - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('doctrine', array( - 'orm' => array( - // ... - 'dql' => array( - 'string_functions' => array( - 'test_string' => 'AppBundle\DQL\StringFunction', - 'second_string' => 'AppBundle\DQL\SecondStringFunction', - ), - 'numeric_functions' => array( - 'test_numeric' => 'AppBundle\DQL\NumericFunction', - ), - 'datetime_functions' => array( - 'test_datetime' => 'AppBundle\DQL\DatetimeFunction', - ), - ), - ), - )); - -.. _`DQL User Defined Functions`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/dql-user-defined-functions.html diff --git a/cookbook/doctrine/file_uploads.rst b/cookbook/doctrine/file_uploads.rst deleted file mode 100644 index 0282086c165..00000000000 --- a/cookbook/doctrine/file_uploads.rst +++ /dev/null @@ -1,574 +0,0 @@ -.. index:: - single: Doctrine; File uploads - -How to Handle File Uploads with Doctrine -======================================== - -.. note:: - - Instead of handling file uploading yourself, you may consider using the - `VichUploaderBundle`_ community bundle. This bundle provides all the common - operations (such as file renaming, saving and deleting) and it's tightly - integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel. - -Handling file uploads with Doctrine entities is no different than handling -any other file upload. In other words, you're free to move the file in your -controller after handling a form submission. For examples of how to do this, -see the :doc:`file type reference ` page. - -If you choose to, you can also integrate the file upload into your entity -lifecycle (i.e. creation, update and removal). In this case, as your entity -is created, updated, and removed from Doctrine, the file uploading and removal -processing will take place automatically (without needing to do anything in -your controller). - -To make this work, you'll need to take care of a number of details, which -will be covered in this cookbook entry. - -Basic Setup ------------ - -First, create a simple Doctrine entity class to work with:: - - // src/AppBundle/Entity/Document.php - namespace AppBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Validator\Constraints as Assert; - - /** - * @ORM\Entity - */ - class Document - { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ - public $id; - - /** - * @ORM\Column(type="string", length=255) - * @Assert\NotBlank - */ - public $name; - - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - public $path; - - public function getAbsolutePath() - { - return null === $this->path - ? null - : $this->getUploadRootDir().'/'.$this->path; - } - - public function getWebPath() - { - return null === $this->path - ? null - : $this->getUploadDir().'/'.$this->path; - } - - protected function getUploadRootDir() - { - // the absolute directory path where uploaded - // documents should be saved - return __DIR__.'/../../../../web/'.$this->getUploadDir(); - } - - protected function getUploadDir() - { - // get rid of the __DIR__ so it doesn't screw up - // when displaying uploaded doc/image in the view. - return 'uploads/documents'; - } - } - -The ``Document`` entity has a name and it is associated with a file. The ``path`` -property stores the relative path to the file and is persisted to the database. -The ``getAbsolutePath()`` is a convenience method that returns the absolute -path to the file while the ``getWebPath()`` is a convenience method that -returns the web path, which can be used in a template to link to the uploaded -file. - -.. tip:: - - If you have not done so already, you should probably read the - :doc:`file ` type documentation first to - understand how the basic upload process works. - -.. note:: - - If you're using annotations to specify your validation rules (as shown - in this example), be sure that you've enabled validation by annotation - (see :ref:`validation configuration `). - -.. caution:: - - If you use the ``getUploadRootDir()`` method, be aware that this will save - the file inside the document root, which can be accessed by everyone. - Consider placing it out of the document root and adding custom viewing - logic when you need to secure the files. - -To handle the actual file upload in the form, use a "virtual" ``file`` field. -For example, if you're building your form directly in a controller, it might -look like this:: - - public function uploadAction() - { - // ... - - $form = $this->createFormBuilder($document) - ->add('name') - ->add('file') - ->getForm(); - - // ... - } - -Next, create this property on your ``Document`` class and add some validation -rules:: - - use Symfony\Component\HttpFoundation\File\UploadedFile; - - // ... - class Document - { - /** - * @Assert\File(maxSize="6000000") - */ - private $file; - - /** - * Sets file. - * - * @param UploadedFile $file - */ - public function setFile(UploadedFile $file = null) - { - $this->file = $file; - } - - /** - * Get file. - * - * @return UploadedFile - */ - public function getFile() - { - return $this->file; - } - } - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Entity/Document.php - namespace AppBundle\Entity; - - // ... - use Symfony\Component\Validator\Constraints as Assert; - - class Document - { - /** - * @Assert\File(maxSize="6000000") - */ - private $file; - - // ... - } - - .. code-block:: yaml - - # src/AppBundle/Resources/config/validation.yml - AppBundle\Entity\Document: - properties: - file: - - File: - maxSize: 6000000 - - .. code-block:: xml - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Entity/Document.php - namespace Acme\DemoBundle\Entity; - - // ... - use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints as Assert; - - class Document - { - // ... - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('file', new Assert\File(array( - 'maxSize' => 6000000, - ))); - } - } - -.. note:: - - As you are using the ``File`` constraint, Symfony will automatically guess - that the form field is a file upload input. That's why you did not have - to set it explicitly when creating the form above (``->add('file')``). - -The following controller shows you how to handle the entire process:: - - // ... - use AppBundle\Entity\Document; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; - use Symfony\Component\HttpFoundation\Request; - // ... - - /** - * @Template() - */ - public function uploadAction(Request $request) - { - $document = new Document(); - $form = $this->createFormBuilder($document) - ->add('name') - ->add('file') - ->getForm(); - - $form->handleRequest($request); - - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - - $em->persist($document); - $em->flush(); - - return $this->redirectToRoute(...); - } - - return array('form' => $form->createView()); - } - -The previous controller will automatically persist the ``Document`` entity -with the submitted name, but it will do nothing about the file and the ``path`` -property will be blank. - -An easy way to handle the file upload is to move it just before the entity is -persisted and then set the ``path`` property accordingly. Start by calling -a new ``upload()`` method on the ``Document`` class, which you'll create -in a moment to handle the file upload:: - - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - - $document->upload(); - - $em->persist($document); - $em->flush(); - - return $this->redirectToRoute(...); - } - -The ``upload()`` method will take advantage of the :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` -object, which is what's returned after a ``file`` field is submitted:: - - public function upload() - { - // the file property can be empty if the field is not required - if (null === $this->getFile()) { - return; - } - - // use the original file name here but you should - // sanitize it at least to avoid any security issues - - // move takes the target directory and then the - // target filename to move to - $this->getFile()->move( - $this->getUploadRootDir(), - $this->getFile()->getClientOriginalName() - ); - - // set the path property to the filename where you've saved the file - $this->path = $this->getFile()->getClientOriginalName(); - - // clean up the file property as you won't need it anymore - $this->file = null; - } - -Using Lifecycle Callbacks -------------------------- - -.. caution:: - - Using lifecycle callbacks is a limited technique that has some drawbacks. - If you want to remove the hardcoded ``__DIR__`` reference inside - the ``Document::getUploadRootDir()`` method, the best way is to start - using explicit :doc:`doctrine listeners `. - There you will be able to inject kernel parameters such as ``kernel.root_dir`` - to be able to build absolute paths. - -Even if this implementation works, it suffers from a major flaw: What if there -is a problem when the entity is persisted? The file would have already moved -to its final location even though the entity's ``path`` property didn't -persist correctly. - -To avoid these issues, you should change the implementation so that the database -operation and the moving of the file become atomic: if there is a problem -persisting the entity or if the file cannot be moved, then *nothing* should -happen. - -To do this, you need to move the file right as Doctrine persists the entity -to the database. This can be accomplished by hooking into an entity lifecycle -callback:: - - /** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ - class Document - { - } - -Next, refactor the ``Document`` class to take advantage of these callbacks:: - - use Symfony\Component\HttpFoundation\File\UploadedFile; - - /** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ - class Document - { - private $temp; - - /** - * Sets file. - * - * @param UploadedFile $file - */ - public function setFile(UploadedFile $file = null) - { - $this->file = $file; - // check if we have an old image path - if (isset($this->path)) { - // store the old name to delete after the update - $this->temp = $this->path; - $this->path = null; - } else { - $this->path = 'initial'; - } - } - - /** - * @ORM\PrePersist() - * @ORM\PreUpdate() - */ - public function preUpload() - { - if (null !== $this->getFile()) { - // do whatever you want to generate a unique name - $filename = sha1(uniqid(mt_rand(), true)); - $this->path = $filename.'.'.$this->getFile()->guessExtension(); - } - } - - /** - * @ORM\PostPersist() - * @ORM\PostUpdate() - */ - public function upload() - { - if (null === $this->getFile()) { - return; - } - - // if there is an error when moving the file, an exception will - // be automatically thrown by move(). This will properly prevent - // the entity from being persisted to the database on error - $this->getFile()->move($this->getUploadRootDir(), $this->path); - - // check if we have an old image - if (isset($this->temp)) { - // delete the old image - unlink($this->getUploadRootDir().'/'.$this->temp); - // clear the temp image path - $this->temp = null; - } - $this->file = null; - } - - /** - * @ORM\PostRemove() - */ - public function removeUpload() - { - $file = $this->getAbsolutePath(); - if ($file) { - unlink($file); - } - } - } - -.. caution:: - - If changes to your entity are handled by a Doctrine event listener or event - subscriber, the ``preUpdate()`` callback must notify Doctrine about the changes - being done. - For full reference on preUpdate event restrictions, see `preUpdate`_ in the - Doctrine Events documentation. - -The class now does everything you need: it generates a unique filename before -persisting, moves the file after persisting, and removes the file if the -entity is ever deleted. - -Now that the moving of the file is handled atomically by the entity, the -call to ``$document->upload()`` should be removed from the controller:: - - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); - - $em->persist($document); - $em->flush(); - - return $this->redirectToRoute(...); - } - -.. note:: - - The ``@ORM\PrePersist()`` and ``@ORM\PostPersist()`` event callbacks are - triggered before and after the entity is persisted to the database. On the - other hand, the ``@ORM\PreUpdate()`` and ``@ORM\PostUpdate()`` event - callbacks are called when the entity is updated. - -.. caution:: - - The ``PreUpdate`` and ``PostUpdate`` callbacks are only triggered if there - is a change in one of the entity's fields that are persisted. This means - that, by default, if you modify only the ``$file`` property, these events - will not be triggered, as the property itself is not directly persisted - via Doctrine. One solution would be to use an ``updated`` field that's - persisted to Doctrine, and to modify it manually when changing the file. - -Using the ``id`` as the Filename --------------------------------- - -If you want to use the ``id`` as the name of the file, the implementation is -slightly different as you need to save the extension under the ``path`` -property, instead of the actual filename:: - - use Symfony\Component\HttpFoundation\File\UploadedFile; - - /** - * @ORM\Entity - * @ORM\HasLifecycleCallbacks - */ - class Document - { - private $temp; - - /** - * Sets file. - * - * @param UploadedFile $file - */ - public function setFile(UploadedFile $file = null) - { - $this->file = $file; - // check if we have an old image path - if (is_file($this->getAbsolutePath())) { - // store the old name to delete after the update - $this->temp = $this->getAbsolutePath(); - } else { - $this->path = 'initial'; - } - } - - /** - * @ORM\PrePersist() - * @ORM\PreUpdate() - */ - public function preUpload() - { - if (null !== $this->getFile()) { - $this->path = $this->getFile()->guessExtension(); - } - } - - /** - * @ORM\PostPersist() - * @ORM\PostUpdate() - */ - public function upload() - { - if (null === $this->getFile()) { - return; - } - - // check if we have an old image - if (isset($this->temp)) { - // delete the old image - unlink($this->temp); - // clear the temp image path - $this->temp = null; - } - - // you must throw an exception here if the file cannot be moved - // so that the entity is not persisted to the database - // which the UploadedFile move() method does - $this->getFile()->move( - $this->getUploadRootDir(), - $this->id.'.'.$this->getFile()->guessExtension() - ); - - $this->setFile(null); - } - - /** - * @ORM\PreRemove() - */ - public function storeFilenameForRemove() - { - $this->temp = $this->getAbsolutePath(); - } - - /** - * @ORM\PostRemove() - */ - public function removeUpload() - { - if (isset($this->temp)) { - unlink($this->temp); - } - } - - public function getAbsolutePath() - { - return null === $this->path - ? null - : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path; - } - } - -You'll notice in this case that you need to do a little bit more work in -order to remove the file. Before it's removed, you must store the file path -(since it depends on the id). Then, once the object has been fully removed -from the database, you can safely delete the file (in ``PostRemove``). - -.. _`preUpdate`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#preupdate -.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle diff --git a/cookbook/doctrine/index.rst b/cookbook/doctrine/index.rst deleted file mode 100644 index a62c736db11..00000000000 --- a/cookbook/doctrine/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -Doctrine -======== - -.. toctree:: - :maxdepth: 2 - - file_uploads - common_extensions - event_listeners_subscribers - dbal - reverse_engineering - multiple_entity_managers - custom_dql_functions - resolve_target_entity - mapping_model_classes - registration_form - console diff --git a/cookbook/doctrine/registration_form.rst b/cookbook/doctrine/registration_form.rst deleted file mode 100644 index b99c22d3a42..00000000000 --- a/cookbook/doctrine/registration_form.rst +++ /dev/null @@ -1,367 +0,0 @@ -.. index:: - single: Doctrine; Simple Registration Form - single: Form; Simple Registration Form - -How to Implement a simple Registration Form -=========================================== - -Some forms have extra fields whose values don't need to be stored in the -database. For example, you may want to create a registration form with some -extra fields (like a "terms accepted" checkbox field) and embed the form -that actually stores the account information. - -The simple User Model ---------------------- - -You have a simple ``User`` entity mapped to the database:: - - // src/Acme/AccountBundle/Entity/User.php - namespace Acme\AccountBundle\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Validator\Constraints as Assert; - use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; - - /** - * @ORM\Entity - * @UniqueEntity(fields="email", message="Email already taken") - */ - class User - { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ - protected $id; - - /** - * @ORM\Column(type="string", length=255) - * @Assert\NotBlank() - * @Assert\Email() - */ - protected $email; - - /** - * @ORM\Column(type="string", length=255) - * @Assert\NotBlank() - * @Assert\Length(max = 4096) - */ - protected $plainPassword; - - public function getId() - { - return $this->id; - } - - public function getEmail() - { - return $this->email; - } - - public function setEmail($email) - { - $this->email = $email; - } - - public function getPlainPassword() - { - return $this->plainPassword; - } - - public function setPlainPassword($password) - { - $this->plainPassword = $password; - } - } - -This ``User`` entity contains three fields and two of them (``email`` and -``plainPassword``) should display on the form. The email property must be unique -in the database, this is enforced by adding this validation at the top of -the class. - -.. note:: - - If you want to integrate this User within the security system, you need - to implement the :ref:`UserInterface ` of the - Security component. - -.. _cookbook-registration-password-max: - -.. sidebar:: Why the 4096 Password Limit? - - Notice that the ``plainPassword`` field has a max length of 4096 characters. - For security purposes (`CVE-2013-5750`_), Symfony limits the plain password - length to 4096 characters when encoding it. Adding this constraint makes - sure that your form will give a validation error if anyone tries a super-long - password. - - You'll need to add this constraint anywhere in your application where - your user submits a plaintext password (e.g. change password form). The - only place where you don't need to worry about this is your login form, - since Symfony's Security component handles this for you. - -Create a Form for the Model ---------------------------- - -Next, create the form for the ``User`` model:: - - // src/Acme/AccountBundle/Form/Type/UserType.php - namespace Acme\AccountBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - use Symfony\Component\OptionsResolver\OptionsResolver; - - class UserType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('email', 'email'); - $builder->add('plainPassword', 'repeated', array( - 'first_name' => 'password', - 'second_name' => 'confirm', - 'type' => 'password', - )); - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults(array( - 'data_class' => 'Acme\AccountBundle\Entity\User' - )); - } - - public function getName() - { - return 'user'; - } - } - -There are just two fields: ``email`` and ``plainPassword`` (repeated to confirm -the entered password). The ``data_class`` option tells the form the name of the -underlying data class (i.e. your ``User`` entity). - -.. tip:: - - To explore more things about the Form component, read :doc:`/book/forms`. - -Embedding the User Form into a Registration Form ------------------------------------------------- - -The form that you'll use for the registration page is not the same as the -form used to simply modify the ``User`` (i.e. ``UserType``). The registration -form will contain further fields like "accept the terms", whose value won't -be stored in the database. - -Start by creating a simple class which represents the "registration":: - - // src/Acme/AccountBundle/Form/Model/Registration.php - namespace Acme\AccountBundle\Form\Model; - - use Symfony\Component\Validator\Constraints as Assert; - - use Acme\AccountBundle\Entity\User; - - class Registration - { - /** - * @Assert\Type(type="Acme\AccountBundle\Entity\User") - * @Assert\Valid() - */ - protected $user; - - /** - * @Assert\NotBlank() - * @Assert\True() - */ - protected $termsAccepted; - - public function setUser(User $user) - { - $this->user = $user; - } - - public function getUser() - { - return $this->user; - } - - public function getTermsAccepted() - { - return $this->termsAccepted; - } - - public function setTermsAccepted($termsAccepted) - { - $this->termsAccepted = (bool) $termsAccepted; - } - } - -Next, create the form for this ``Registration`` model:: - - // src/Acme/AccountBundle/Form/Type/RegistrationType.php - namespace Acme\AccountBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class RegistrationType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('user', new UserType()); - $builder->add( - 'terms', - 'checkbox', - array('property_path' => 'termsAccepted') - ); - $builder->add('Register', 'submit'); - } - - public function getName() - { - return 'registration'; - } - } - -You don't need to use a special method for embedding the ``UserType`` form. -A form is a field, too - so you can add this like any other field, with the -expectation that the ``Registration.user`` property will hold an instance -of the ``User`` class. - -Handling the Form Submission ----------------------------- - -Next, you need a controller to handle the form. Start by creating a simple -controller for displaying the registration form:: - - // src/Acme/AccountBundle/Controller/AccountController.php - namespace Acme\AccountBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - - use Acme\AccountBundle\Form\Type\RegistrationType; - use Acme\AccountBundle\Form\Model\Registration; - - class AccountController extends Controller - { - public function registerAction() - { - $registration = new Registration(); - $form = $this->createForm(new RegistrationType(), $registration, array( - 'action' => $this->generateUrl('account_create'), - )); - - return $this->render( - 'AcmeAccountBundle:Account:register.html.twig', - array('form' => $form->createView()) - ); - } - } - -And its template: - -.. code-block:: html+jinja - - {# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #} - {{ form(form) }} - -Next, create the controller which handles the form submission. This performs -the validation and saves the data into the database:: - - use Symfony\Component\HttpFoundation\Request; - // ... - - public function createAction(Request $request) - { - $em = $this->getDoctrine()->getManager(); - - $form = $this->createForm(new RegistrationType(), new Registration()); - - $form->handleRequest($request); - - if ($form->isValid()) { - $registration = $form->getData(); - - $em->persist($registration->getUser()); - $em->flush(); - - return $this->redirectToRoute(...); - } - - return $this->render( - 'AcmeAccountBundle:Account:register.html.twig', - array('form' => $form->createView()) - ); - } - -Add new Routes --------------- - -Next, update your routes. If you're placing your routes inside your bundle -(as shown here), don't forget to make sure that the routing file is being -:ref:`imported `. - -.. configuration-block:: - - .. code-block:: yaml - - # src/Acme/AccountBundle/Resources/config/routing.yml - account_register: - path: /register - defaults: { _controller: AcmeAccountBundle:Account:register } - - account_create: - path: /register/create - defaults: { _controller: AcmeAccountBundle:Account:create } - - .. code-block:: xml - - - - - - - AcmeAccountBundle:Account:register - - - - AcmeAccountBundle:Account:create - - - - .. code-block:: php - - // src/Acme/AccountBundle/Resources/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('account_register', new Route('/register', array( - '_controller' => 'AcmeAccountBundle:Account:register', - ))); - $collection->add('account_create', new Route('/register/create', array( - '_controller' => 'AcmeAccountBundle:Account:create', - ))); - - return $collection; - -Update your Database Schema ---------------------------- - -Of course, since you've added a ``User`` entity during this tutorial, make -sure that your database schema has been updated properly: - -.. code-block:: bash - - $ php app/console doctrine:schema:update --force - -That's it! Your form now validates, and allows you to save the ``User`` -object to the database. The extra ``terms`` checkbox on the ``Registration`` -model class is used during validation, but not actually used afterwards when -saving the User to the database. - -.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form diff --git a/cookbook/email/gmail.rst b/cookbook/email/gmail.rst deleted file mode 100644 index 9f925cad224..00000000000 --- a/cookbook/email/gmail.rst +++ /dev/null @@ -1,87 +0,0 @@ -.. index:: - single: Emails; Gmail - -How to Use Gmail to Send Emails -=============================== - -During development, instead of using a regular SMTP server to send emails, you -might find using Gmail easier and more practical. The SwiftmailerBundle makes -it really easy. - -.. tip:: - - Instead of using your regular Gmail account, it's of course recommended - that you create a special account. - -In the development configuration file, change the ``transport`` setting to -``gmail`` and set the ``username`` and ``password`` to the Google credentials: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - swiftmailer: - transport: gmail - username: your_gmail_username - password: your_gmail_password - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('swiftmailer', array( - 'transport' => 'gmail', - 'username' => 'your_gmail_username', - 'password' => 'your_gmail_password', - )); - -You're done! - -.. tip:: - - If you are using the Symfony Standard Edition, configure the parameters in ``parameters.yml``: - - .. code-block:: yaml - - # app/config/parameters.yml - parameters: - # ... - mailer_transport: gmail - mailer_host: ~ - mailer_user: your_gmail_username - mailer_password: your_gmail_password - -.. note:: - - The ``gmail`` transport is simply a shortcut that uses the ``smtp`` transport - and sets ``encryption``, ``auth_mode`` and ``host`` to work with Gmail. - -.. note:: - - Depending on your Gmail account settings, you may get authentication errors - within your app. If your Gmail account uses 2-Step-Verification, you should - `generate an App password`_ to use for your ``mailer_password`` parameter. - You should also ensure that you `allow less secure apps to access your Gmail account`_. - -.. _`generate an App password`: https://support.google.com/accounts/answer/185833 -.. _`allow less secure apps to access your Gmail account`: https://support.google.com/accounts/answer/6010255 diff --git a/cookbook/email/index.rst b/cookbook/email/index.rst deleted file mode 100644 index 351301f03e6..00000000000 --- a/cookbook/email/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Email -===== - -.. toctree:: - :maxdepth: 2 - - email - gmail - cloud - dev_environment - spool - testing diff --git a/cookbook/event_dispatcher/index.rst b/cookbook/event_dispatcher/index.rst deleted file mode 100644 index 8dfe9a541f4..00000000000 --- a/cookbook/event_dispatcher/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Event Dispatcher -================ - -.. toctree:: - :maxdepth: 2 - - before_after_filters - class_extension - method_behavior diff --git a/cookbook/expression/index.rst b/cookbook/expression/index.rst deleted file mode 100644 index 909ecc72224..00000000000 --- a/cookbook/expression/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Expressions -=========== - -.. toctree:: - :maxdepth: 2 - - expressions diff --git a/cookbook/form/index.rst b/cookbook/form/index.rst deleted file mode 100644 index 5aea7405d4c..00000000000 --- a/cookbook/form/index.rst +++ /dev/null @@ -1,21 +0,0 @@ -Form -==== - -.. toctree:: - :maxdepth: 2 - - form_customization - data_transformers - dynamic_form_modification - form_collections - create_custom_field_type - create_form_type_extension - inherit_data_option - unit_testing - use_empty_data - direct_submit - -.. toctree:: - :hidden: - - use_virtuals_forms diff --git a/cookbook/frontend/bower.rst b/cookbook/frontend/bower.rst deleted file mode 100644 index c12730e0646..00000000000 --- a/cookbook/frontend/bower.rst +++ /dev/null @@ -1,146 +0,0 @@ -.. index:: - single: Front-end; Bower - -Using Bower with Symfony -======================== - -Symfony and all its packages are perfectly managed by Composer. Bower is a -dependency management tool for front-end dependencies, like Bootstrap or -jQuery. As Symfony is purely a back-end framework, it can't help you much with -Bower. Fortunately, it is very easy to use! - -Installing Bower ----------------- - -Bower_ is built on top of `Node.js`_. Make sure you have that installed and -then run: - -.. code-block:: bash - - $ npm install -g bower - -After this command has finished, run ``bower`` in your terminal to find out if -it's installed correctly. - -.. tip:: - - If you don't want to have NodeJS on your computer, you can also use - BowerPHP_ (an unofficial PHP port of Bower). Beware that this is still in - an alpha state. If you're using BowerPHP, use ``bowerphp`` instead of - ``bower`` in the examples. - -Configuring Bower in your Project ---------------------------------- - -Normally, Bower downloads everything into a ``bower_components/`` directory. In -Symfony, only files in the ``web/`` directory are publicly accessible, so you -need to configure Bower to download things there instead. To do that, just -create a ``.bowerrc`` file with a new destination (like ``web/assets/vendor``): - -.. code-block:: json - - { - "directory": "web/assets/vendor/" - } - -.. tip:: - - If you're using a front-end build system like `Gulp`_ or `Grunt`_, then - you can set the directory to whatever you want. Typically, you'll use - these tools to ultimately move all assets into the ``web/`` directory. - -An Example: Installing Bootstrap --------------------------------- - -Believe it or not, but you're now ready to use Bower in your Symfony -application. As an example, you'll now install Bootstrap in your project and -include it in your layout. - -Installing the Dependency -~~~~~~~~~~~~~~~~~~~~~~~~~ - -To create a ``bower.json`` file, just run ``bower init``. Now you're ready to -start adding things to your project. For example, to add Bootstrap_ to your -``bower.json`` and download it, just run: - -.. code-block:: bash - - $ bower install --save bootstrap - -This will install Bootstrap and its dependencies in ``web/assets/vendor/`` (or -whatever directory you configured in ``.bowerrc``). - -.. seealso:: - - For more details on how to use Bower, check out `Bower documentation`_. - -Including the Dependency in your Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now that the dependencies are installed, you can include bootstrap in your -template like normal CSS/JS: - -.. configuration-block:: - - .. code-block:: html+jinja - - {# app/Resources/views/layout.html.twig #} - - - - {# ... #} - - - - - {# ... #} - - - .. code-block:: html+php - - - - - - {# ... #} - - - - - {# ... #} - - -Great job! Your site is now using Bootstrap. You can now easily upgrade -bootstrap to the latest version and manage other front-end dependencies too. - -Should I Git Ignore or Commit Bower Assets? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Currently, you should probably *commit* the assets downloaded by Bower instead -of adding the directory (e.g. ``web/assets/vendor``) to your ``.gitignore`` -file: - -.. code-block:: bash - - $ git add web/assets/vendor - -Why? Unlike Composer, Bower currently does not have a "lock" feature, which -means that there's no guarantee that running ``bower install`` on a different -server will give you the *exact* assets that you have on other machines. -For more details, read the article `Checking in front-end dependencies`_. - -But, it's very possible that Bower will add a lock feature in the future -(e.g. `bower/bower#1748`_). - -.. _Bower: http://bower.io -.. _`Node.js`: https://nodejs.org -.. _BowerPHP: http://bowerphp.org/ -.. _`Bower documentation`: http://bower.io/ -.. _Bootstrap: http://getbootstrap.com/ -.. _Gulp: http://gulpjs.com/ -.. _Grunt: http://gruntjs.com/ -.. _`Checking in front-end dependencies`: http://addyosmani.com/blog/checking-in-front-end-dependencies/ -.. _`bower/bower#1748`: https://github.com/bower/bower/pull/1748 diff --git a/cookbook/frontend/index.rst b/cookbook/frontend/index.rst deleted file mode 100644 index 3bfa8e75b17..00000000000 --- a/cookbook/frontend/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Front-end -========= - -.. toctree:: - :maxdepth: 2 - - bower diff --git a/cookbook/index.rst b/cookbook/index.rst deleted file mode 100644 index bdf414d6177..00000000000 --- a/cookbook/index.rst +++ /dev/null @@ -1,41 +0,0 @@ -The Cookbook -============ - -.. toctree:: - :hidden: - - assetic/index - bundles/index - cache/index - composer - configuration/index - console/index - controller/index - debugging - deployment/index - doctrine/index - email/index - event_dispatcher/index - expression/index - form/index - frontend/index - install/index - logging/index - profiler/index - request/index - routing/index - security/index - serializer - service_container/index - session/index - psr7 - symfony1 - templating/index - testing/index - upgrade/index - validation/index - web_server/index - web_services/index - workflow/index - -.. include:: /cookbook/map.rst.inc diff --git a/cookbook/install/index.rst b/cookbook/install/index.rst deleted file mode 100644 index 8ec509592f9..00000000000 --- a/cookbook/install/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Install and Upgrade -=================== - -.. toctree:: - :maxdepth: 2 - - unstable_versions diff --git a/cookbook/logging/index.rst b/cookbook/logging/index.rst deleted file mode 100644 index 2570b3d5627..00000000000 --- a/cookbook/logging/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Logging -======= - -.. toctree:: - :maxdepth: 2 - - monolog - monolog_email - monolog_console - monolog_regex_based_excludes - channels_handlers diff --git a/cookbook/logging/monolog.rst b/cookbook/logging/monolog.rst deleted file mode 100644 index 5ebe688433a..00000000000 --- a/cookbook/logging/monolog.rst +++ /dev/null @@ -1,538 +0,0 @@ -.. index:: - single: Logging - -How to Use Monolog to Write Logs -================================ - -Monolog_ is a logging library for PHP used by Symfony. It is inspired by the -Python LogBook library. - -Usage ------ - -To log a message simply get the ``logger`` service from the container in -your controller:: - - public function indexAction() - { - $logger = $this->get('logger'); - $logger->info('I just got the logger'); - $logger->error('An error occurred'); - - // ... - } - -The ``logger`` service has different methods for different logging levels. -See LoggerInterface_ for details on which methods are available. - -Handlers and Channels: Writing Logs to different Locations ----------------------------------------------------------- - -In Monolog each logger defines a logging channel, which organizes your log -messages into different "categories". Then, each channel has a stack of handlers -to write the logs (the handlers can be shared). - -.. tip:: - - When injecting the logger in a service you can - :ref:`use a custom channel ` control which "channel" - the logger will log to. - -The basic handler is the ``StreamHandler`` which writes logs in a stream -(by default in the ``app/logs/prod.log`` in the prod environment and -``app/logs/dev.log`` in the dev environment). - -Monolog comes also with a powerful built-in handler for the logging in -prod environment: ``FingersCrossedHandler``. It allows you to store the -messages in a buffer and to log them only if a message reaches the -action level (``error`` in the configuration provided in the Symfony Standard -Edition) by forwarding the messages to another handler. - -Using several Handlers -~~~~~~~~~~~~~~~~~~~~~~ - -The logger uses a stack of handlers which are called successively. This -allows you to log the messages in several ways easily. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - monolog: - handlers: - applog: - type: stream - path: /var/log/symfony.log - level: error - main: - type: fingers_crossed - action_level: warning - handler: file - file: - type: stream - level: debug - syslog: - type: syslog - level: error - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'applog' => array( - 'type' => 'stream', - 'path' => '/var/log/symfony.log', - 'level' => 'error', - ), - 'main' => array( - 'type' => 'fingers_crossed', - 'action_level' => 'warning', - 'handler' => 'file', - ), - 'file' => array( - 'type' => 'stream', - 'level' => 'debug', - ), - 'syslog' => array( - 'type' => 'syslog', - 'level' => 'error', - ), - ), - )); - -The above configuration defines a stack of handlers which will be called -in the order they are defined. - -.. tip:: - - The handler named "file" will not be included in the stack itself as - it is used as a nested handler of the ``fingers_crossed`` handler. - -.. note:: - - If you want to change the config of MonologBundle in another config - file you need to redefine the whole stack. It cannot be merged - because the order matters and a merge does not allow to control the - order. - -Changing the Formatter -~~~~~~~~~~~~~~~~~~~~~~ - -The handler uses a ``Formatter`` to format the record before logging -it. All Monolog handlers use an instance of -``Monolog\Formatter\LineFormatter`` by default but you can replace it -easily. Your formatter must implement -``Monolog\Formatter\FormatterInterface``. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - my_formatter: - class: Monolog\Formatter\JsonFormatter - monolog: - handlers: - file: - type: stream - level: debug - formatter: my_formatter - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register('my_formatter', 'Monolog\Formatter\JsonFormatter'); - - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'file' => array( - 'type' => 'stream', - 'level' => 'debug', - 'formatter' => 'my_formatter', - ), - ), - )); - -How to Rotate your Log Files ----------------------------- - -Over time, log files can grow to be *huge*, both while developing and on -production. One best-practice solution is to use a tool like the `logrotate`_ -Linux command to rotate log files before they become too large. - -Another option is to have Monolog rotate the files for you by using the -``rotating_file`` handler. This handler creates a new log file every day -and can also remove old files automatically. To use it, just set the ``type`` -option of your handler to ``rotating_file``: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_dev.yml - monolog: - handlers: - main: - type: rotating_file - path: %kernel.logs_dir%/%kernel.environment%.log - level: debug - # max number of log files to keep - # defaults to zero, which means infinite files - max_files: 10 - - .. code-block:: xml - - - - - - - - max_files="10" - /> - - - - .. code-block:: php - - // app/config/config_dev.php - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'main' => array( - 'type' => 'rotating_file', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - // max number of log files to keep - // defaults to zero, which means infinite files - 'max_files' => 10, - ), - ), - )); - -Adding some extra Data in the Log Messages ------------------------------------------- - -Monolog allows you to process the record before logging it to add some -extra data. A processor can be applied for the whole handler stack or -only for a specific handler. - -A processor is simply a callable receiving the record as its first argument. -Processors are configured using the ``monolog.processor`` DIC tag. See the -:ref:`reference about it `. - -Adding a Session/Request Token -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Sometimes it is hard to tell which entries in the log belong to which session -and/or request. The following example will add a unique token for each request -using a processor. - -.. code-block:: php - - namespace Acme\MyBundle; - - use Symfony\Component\HttpFoundation\Session\Session; - - class SessionRequestProcessor - { - private $session; - private $token; - - public function __construct(Session $session) - { - $this->session = $session; - } - - public function processRecord(array $record) - { - if (null === $this->token) { - try { - $this->token = substr($this->session->getId(), 0, 8); - } catch (\RuntimeException $e) { - $this->token = '????????'; - } - $this->token .= '-' . substr(uniqid(), -8); - } - $record['extra']['token'] = $this->token; - - return $record; - } - } - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - monolog.formatter.session_request: - class: Monolog\Formatter\LineFormatter - arguments: - - "[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n" - - monolog.processor.session_request: - class: Acme\MyBundle\SessionRequestProcessor - arguments: ["@session"] - tags: - - { name: monolog.processor, method: processRecord } - - monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - formatter: monolog.formatter.session_request - - .. code-block:: xml - - - - - - - - - [%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%% - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register( - 'monolog.formatter.session_request', - 'Monolog\Formatter\LineFormatter' - ) - ->addArgument('[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n'); - - $container - ->register( - 'monolog.processor.session_request', - 'Acme\MyBundle\SessionRequestProcessor' - ) - ->addArgument(new Reference('session')) - ->addTag('monolog.processor', array('method' => 'processRecord')); - - $container->loadFromExtension('monolog', array( - 'handlers' => array( - 'main' => array( - 'type' => 'stream', - 'path' => '%kernel.logs_dir%/%kernel.environment%.log', - 'level' => 'debug', - 'formatter' => 'monolog.formatter.session_request', - ), - ), - )); - -.. note:: - - If you use several handlers, you can also register a processor at the - handler level or at the channel level instead of registering it globally - (see the following sections). - -Registering Processors per Handler ----------------------------------- - -You can register a processor per handler using the ``handler`` option of -the ``monolog.processor`` tag: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - monolog.processor.session_request: - class: Acme\MyBundle\SessionRequestProcessor - arguments: ["@session"] - tags: - - { name: monolog.processor, method: processRecord, handler: main } - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register( - 'monolog.processor.session_request', - 'Acme\MyBundle\SessionRequestProcessor' - ) - ->addArgument(new Reference('session')) - ->addTag('monolog.processor', array('method' => 'processRecord', 'handler' => 'main')); - -Registering Processors per Channel ----------------------------------- - -You can register a processor per channel using the ``channel`` option of -the ``monolog.processor`` tag: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - services: - monolog.processor.session_request: - class: Acme\MyBundle\SessionRequestProcessor - arguments: ["@session"] - tags: - - { name: monolog.processor, method: processRecord, channel: main } - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // app/config/config.php - $container - ->register( - 'monolog.processor.session_request', - 'Acme\MyBundle\SessionRequestProcessor' - ) - ->addArgument(new Reference('session')) - ->addTag('monolog.processor', array('method' => 'processRecord', 'channel' => 'main')); - -.. _Monolog: https://github.com/Seldaek/monolog -.. _LoggerInterface: https://github.com/php-fig/log/blob/master/Psr/Log/LoggerInterface.php -.. _`logrotate`: https://fedorahosted.org/logrotate/ diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc deleted file mode 100644 index b7563d7f6b8..00000000000 --- a/cookbook/map.rst.inc +++ /dev/null @@ -1,259 +0,0 @@ -* :doc:`/cookbook/assetic/index` - - * :doc:`/cookbook/assetic/asset_management` - * :doc:`/cookbook/assetic/php` - * :doc:`/cookbook/assetic/uglifyjs` - * :doc:`/cookbook/assetic/yuicompressor` - * :doc:`/cookbook/assetic/jpeg_optimize` - * :doc:`/cookbook/assetic/apply_to_option` - -* :doc:`/cookbook/bundles/index` - - * :doc:`/cookbook/bundles/installation` - * :doc:`/cookbook/bundles/best_practices` - * :doc:`/cookbook/bundles/inheritance` - * :doc:`/cookbook/bundles/override` - * :doc:`/cookbook/bundles/remove` - * :doc:`/cookbook/bundles/extension` - * :doc:`/cookbook/bundles/configuration` - * :doc:`/cookbook/bundles/prepend_extension` - -* :doc:`/cookbook/cache/index` - - * :doc:`/cookbook/cache/varnish` - * :doc:`/cookbook/cache/form_csrf_caching` - -* **Composer** - - * :doc:`/cookbook/composer` - -* :doc:`/cookbook/configuration/index` - - * :doc:`/cookbook/configuration/environments` - * :doc:`/cookbook/configuration/override_dir_structure` - * :doc:`/cookbook/configuration/using_parameters_in_dic` - * :doc:`/cookbook/configuration/front_controllers_and_kernel` - * :doc:`/cookbook/configuration/external_parameters` - * :doc:`/cookbook/configuration/pdo_session_storage` - * :doc:`/cookbook/configuration/apache_router` - * :doc:`/cookbook/configuration/web_server_configuration` - * :doc:`/cookbook/configuration/configuration_organization` - * :doc:`/cookbook/configuration/mongodb_session_storage` - -* :doc:`/cookbook/console/index` - - * :doc:`/cookbook/console/console_command` - * :doc:`/cookbook/console/usage` - * :doc:`/cookbook/console/command_in_controller` - * :doc:`/cookbook/console/sending_emails` - * :doc:`/cookbook/console/logging` - * :doc:`/cookbook/console/commands_as_services` - -* :doc:`/cookbook/controller/index` - - * :doc:`/cookbook/controller/error_pages` - * :doc:`/cookbook/controller/service` - * :doc:`/cookbook/controller/upload_file` - -* **Debugging** - - * :doc:`/cookbook/debugging` - -* :doc:`/cookbook/deployment/index` - - * :doc:`/cookbook/deployment/tools` - * :doc:`/cookbook/deployment/azure-website` - * :doc:`/cookbook/deployment/heroku` - * :doc:`/cookbook/deployment/platformsh` - -* :doc:`/cookbook/doctrine/index` - - * :doc:`/cookbook/doctrine/file_uploads` - * :doc:`/cookbook/doctrine/common_extensions` - * :doc:`/cookbook/doctrine/event_listeners_subscribers` - * :doc:`/cookbook/doctrine/dbal` - * :doc:`/cookbook/doctrine/reverse_engineering` - * :doc:`/cookbook/doctrine/multiple_entity_managers` - * :doc:`/cookbook/doctrine/custom_dql_functions` - * :doc:`/cookbook/doctrine/resolve_target_entity` - * :doc:`/cookbook/doctrine/mapping_model_classes` - * :doc:`/cookbook/doctrine/registration_form` - * :doc:`/cookbook/doctrine/console` - * (configuration) :doc:`/cookbook/configuration/pdo_session_storage` - -* :doc:`/cookbook/email/index` - - * :doc:`/cookbook/email/email` - * :doc:`/cookbook/email/gmail` - * :doc:`/cookbook/email/cloud` - * :doc:`/cookbook/email/dev_environment` - * :doc:`/cookbook/email/spool` - * :doc:`/cookbook/email/testing` - -* :doc:`/cookbook/event_dispatcher/index` - - * :doc:`/cookbook/event_dispatcher/before_after_filters` - * :doc:`/cookbook/event_dispatcher/class_extension` - * :doc:`/cookbook/event_dispatcher/method_behavior` - * (service container) :doc:`/cookbook/service_container/event_listener` - -* :doc:`/cookbook/expression/index` - - * :doc:`/cookbook/expression/expressions` - -* :doc:`/cookbook/form/index` - - * :doc:`/cookbook/form/form_customization` - * :doc:`/cookbook/form/data_transformers` - * :doc:`/cookbook/form/dynamic_form_modification` - * :doc:`/cookbook/form/form_collections` - * :doc:`/cookbook/form/create_custom_field_type` - * :doc:`/cookbook/form/create_form_type_extension` - * :doc:`/cookbook/form/inherit_data_option` - * :doc:`/cookbook/form/unit_testing` - * :doc:`/cookbook/form/use_empty_data` - * :doc:`/cookbook/form/direct_submit` - * (validation) :doc:`/cookbook/validation/custom_constraint` - * (doctrine) :doc:`/cookbook/doctrine/file_uploads` - -* :doc:`/cookbook/frontend/index` - - * :doc:`/cookbook/frontend/bower` - -* :doc:`/cookbook/install/index` - - * :doc:`/cookbook/install/unstable_versions` - -* :doc:`/cookbook/logging/index` - - * :doc:`/cookbook/logging/monolog` - * :doc:`/cookbook/logging/monolog_email` - * :doc:`/cookbook/logging/monolog_console` - * :doc:`/cookbook/logging/monolog_regex_based_excludes` - * :doc:`/cookbook/logging/channels_handlers` - -* :doc:`/cookbook/profiler/index` - - * :doc:`/cookbook/profiler/data_collector` - * :doc:`/cookbook/profiler/matchers` - * :doc:`/cookbook/profiler/storage` - * :doc:`/cookbook/profiler/profiling_data` - -* :doc:`/cookbook/request/index` - - * :doc:`/cookbook/request/load_balancer_reverse_proxy` - * :doc:`/cookbook/request/mime_type` - * (session) :doc:`/cookbook/session/locale_sticky_session` - -* :doc:`/cookbook/routing/index` - - * :doc:`/cookbook/routing/scheme` - * :doc:`/cookbook/routing/slash_in_parameter` - * :doc:`/cookbook/routing/redirect_in_config` - * :doc:`/cookbook/routing/method_parameters` - * :doc:`/cookbook/routing/service_container_parameters` - * :doc:`/cookbook/routing/custom_route_loader` - * :doc:`/cookbook/routing/redirect_trailing_slash` - * :doc:`/cookbook/routing/extra_information` - -* :doc:`Security Authentication (Identifying/Logging in the User) ` - - * :doc:`/cookbook/security/form_login_setup` - * :doc:`/cookbook/security/entity_provider` - * :doc:`/cookbook/security/remember_me` - * :doc:`/cookbook/security/impersonating_user` - * :doc:`/cookbook/security/form_login` - * :doc:`/cookbook/security/custom_provider` - * :doc:`/cookbook/security/custom_password_authenticator` - * :doc:`/cookbook/security/api_key_authentication` - * :doc:`/cookbook/security/custom_authentication_provider` - * :doc:`/cookbook/security/pre_authenticated` - * :doc:`/cookbook/security/target_path` - * :doc:`/cookbook/security/csrf_in_login_form` - * :doc:`/cookbook/security/named_encoders` - * :doc:`/cookbook/security/multiple_user_providers` - * :doc:`/cookbook/security/firewall_restriction` - * :doc:`/cookbook/security/host_restriction` - -* :doc:`Security Authorization (Denying Access) ` - - * :doc:`/cookbook/security/voters` - * :doc:`/cookbook/security/acl` - * :doc:`/cookbook/security/acl_advanced` - * :doc:`/cookbook/security/force_https` - * :doc:`/cookbook/security/securing_services` - * :doc:`/cookbook/security/access_control` - -* **Serializer** - - * :doc:`/cookbook/serializer` - -* :doc:`/cookbook/service_container/index` - - * :doc:`/cookbook/service_container/event_listener` - * :doc:`/cookbook/service_container/scopes` - * :doc:`/cookbook/service_container/compiler_passes` - -* :doc:`/cookbook/session/index` - - * :doc:`/cookbook/session/proxy_examples` - * :doc:`/cookbook/session/locale_sticky_session` - * :doc:`/cookbook/session/sessions_directory` - * :doc:`/cookbook/session/php_bridge` - * :doc:`/cookbook/session/limit_metadata_writes` - * (configuration) :doc:`/cookbook/configuration/pdo_session_storage` - * (configuration) :doc:`/cookbook/configuration/mongodb_session_storage` - * :doc:`/cookbook/session/avoid_session_start` - -* **PSR-7** - - * :doc:`/cookbook/psr7` - -* **symfony1** - - * :doc:`/cookbook/symfony1` - -* :doc:`/cookbook/templating/index` - - * :doc:`/cookbook/templating/global_variables` - * :doc:`/cookbook/templating/namespaced_paths` - * :doc:`/cookbook/templating/PHP` - * :doc:`/cookbook/templating/twig_extension` - * :doc:`/cookbook/templating/render_without_controller` - -* :doc:`/cookbook/testing/index` - - * :doc:`/cookbook/testing/http_authentication` - * :doc:`/cookbook/testing/simulating_authentication` - * :doc:`/cookbook/testing/insulating_clients` - * :doc:`/cookbook/testing/profiling` - * :doc:`/cookbook/testing/database` - * :doc:`/cookbook/testing/doctrine` - * :doc:`/cookbook/testing/bootstrap` - * (email) :doc:`/cookbook/email/testing` - * (form) :doc:`/cookbook/form/unit_testing` - -* :doc:`/cookbook/upgrade/index` - - * :doc:`/cookbook/upgrade/patch_version` - * :doc:`/cookbook/upgrade/minor_version` - * :doc:`/cookbook/upgrade/major_version` - -* :doc:`/cookbook/validation/index` - - * :doc:`/cookbook/validation/custom_constraint` - * :doc:`/cookbook/validation/severity` - -* :doc:`/cookbook/web_server/index` - - * :doc:`/cookbook/web_server/built_in` - * (configuration) :doc:`/cookbook/configuration/web_server_configuration` - -* :doc:`/cookbook/web_services/index` - - * :doc:`/cookbook/web_services/php_soap_extension` - -* :doc:`/cookbook/workflow/index` - - * :doc:`/cookbook/workflow/new_project_git` - * :doc:`/cookbook/workflow/new_project_svn` diff --git a/cookbook/profiler/data_collector.rst b/cookbook/profiler/data_collector.rst deleted file mode 100644 index 985738aa5cb..00000000000 --- a/cookbook/profiler/data_collector.rst +++ /dev/null @@ -1,215 +0,0 @@ -.. index:: - single: Profiling; Data collector - -How to Create a custom Data Collector -===================================== - -:doc:`The Symfony Profiler ` delegates data collecting to -data collectors. Symfony comes bundled with a few of them, but you can easily -create your own. - -Creating a custom Data Collector --------------------------------- - -Creating a custom data collector is as simple as implementing the -:class:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface`:: - - interface DataCollectorInterface - { - /** - * Collects data for the given Request and Response. - * - * @param Request $request A Request instance - * @param Response $response A Response instance - * @param \Exception $exception An Exception instance - */ - function collect(Request $request, Response $response, \Exception $exception = null); - - /** - * Returns the name of the collector. - * - * @return string The collector name - */ - function getName(); - } - -The ``getName()`` method must return a unique name. This is used to access the -information later on (see :doc:`/cookbook/testing/profiling` for -instance). - -The ``collect()`` method is responsible for storing the data it wants to give -access to in local properties. - -.. caution:: - - As the profiler serializes data collector instances, you should not - store objects that cannot be serialized (like PDO objects), or you need - to provide your own ``serialize()`` method. - -Most of the time, it is convenient to extend -:class:`Symfony\\Component\\HttpKernel\\DataCollector\\DataCollector` and -populate the ``$this->data`` property (it takes care of serializing the -``$this->data`` property):: - - class MemoryDataCollector extends DataCollector - { - public function collect(Request $request, Response $response, \Exception $exception = null) - { - $this->data = array( - 'memory' => memory_get_peak_usage(true), - ); - } - - public function getMemory() - { - return $this->data['memory']; - } - - public function getName() - { - return 'memory'; - } - } - -.. _data_collector_tag: - -Enabling custom Data Collectors -------------------------------- - -To enable a data collector, add it as a regular service in one of your -configuration, and tag it with ``data_collector``: - -.. configuration-block:: - - .. code-block:: yaml - - services: - data_collector.your_collector_name: - class: Fully\Qualified\Collector\Class\Name - tags: - - { name: data_collector } - - .. code-block:: xml - - - - - - .. code-block:: php - - $container - ->register('data_collector.your_collector_name', 'Fully\Qualified\Collector\Class\Name') - ->addTag('data_collector') - ; - -Adding Web Profiler Templates ------------------------------ - -When you want to display the data collected by your data collector in the web -debug toolbar or the web profiler, you will need to create a Twig template. The -following example can help you get started: - -.. code-block:: jinja - - {% extends 'WebProfilerBundle:Profiler:layout.html.twig' %} - - {% block toolbar %} - {# This toolbar item may appear along the top or bottom of the screen.#} - {% set icon %} - - Example - {% endset %} - - {% set text %} -
- Quick piece of data - 100 units -
-
- Another quick thing - 300 units -
- {% endset %} - - {# Set the "link" value to false if you do not have a big "panel" - section that you want to direct the user to. #} - {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': true } %} - - {% endblock %} - - {% block head %} - {# Optional, if you need your own JS or CSS files. #} - {{ parent() }} {# Use parent() to keep the default styles #} - {% endblock %} - - {% block menu %} - {# This left-hand menu appears when using the full-screen profiler. #} - - - Example Collector - - {% endblock %} - - {% block panel %} - {# Optional, for showing the most details. #} -

Example

-

- Major information goes here -

- {% endblock %} - -Each block is optional. The ``toolbar`` block is used for the web debug -toolbar and ``menu`` and ``panel`` are used to add a panel to the web -profiler. - -All blocks have access to the ``collector`` object. - -.. tip:: - - Built-in templates use a base64 encoded image for the toolbar: - - .. code-block:: html - - - - You can easily calculate the base64 value for an image with this - little script:: - - #!/usr/bin/env php - - - - - .. code-block:: php - - $container - ->register('data_collector.your_collector_name', 'Acme\DebugBundle\Collector\Class\Name') - ->addTag('data_collector', array( - 'template' => 'AcmeDebugBundle:Collector:templatename', - 'id' => 'your_collector_name', - )) - ; - -.. caution:: - - Make sure the ``id`` attribute is the same string you used for the - ``getName()`` method. diff --git a/cookbook/profiler/index.rst b/cookbook/profiler/index.rst deleted file mode 100644 index b5fb1091099..00000000000 --- a/cookbook/profiler/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Profiler -======== - -.. toctree:: - :maxdepth: 2 - - data_collector - matchers - storage - profiling_data diff --git a/cookbook/profiler/matchers.rst b/cookbook/profiler/matchers.rst deleted file mode 100644 index e27ea0f8457..00000000000 --- a/cookbook/profiler/matchers.rst +++ /dev/null @@ -1,165 +0,0 @@ -.. index:: - single: Profiling; Matchers - -How to Use Matchers to Enable the Profiler Conditionally -======================================================== - -By default, the profiler is only activated in the development environment. But -it's imaginable that a developer may want to see the profiler even in -production. Another situation may be that you want to show the profiler only -when an admin has logged in. You can enable the profiler in these situations -by using matchers. - -Using the built-in Matcher --------------------------- - -Symfony provides a -:class:`built-in matcher ` -which can match paths and IPs. For example, if you want to only show the -profiler when accessing the page with the ``168.0.0.1`` IP, then you can -use this configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - profiler: - matcher: - ip: 168.0.0.1 - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'profiler' => array( - 'ip' => '168.0.0.1', - ), - )); - -You can also set a ``path`` option to define the path on which the profiler -should be enabled. For instance, setting it to ``^/admin/`` will enable the -profiler only for the ``/admin/`` URLs. - -Creating a custom Matcher -------------------------- - -You can also create a custom matcher. This is a service that checks whether -the profiler should be enabled or not. To create that service, create a class -which implements -:class:`Symfony\\Component\\HttpFoundation\\RequestMatcherInterface`. This -interface requires one method: -:method:`Symfony\\Component\\HttpFoundation\\RequestMatcherInterface::matches`. -This method returns false to disable the profiler and true to enable the -profiler. - -To enable the profiler when a ``ROLE_SUPER_ADMIN`` is logged in, you can use -something like:: - - // src/AppBundle/Profiler/SuperAdminMatcher.php - namespace AppBundle\Profiler; - - use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\HttpFoundation\RequestMatcherInterface; - - class SuperAdminMatcher implements RequestMatcherInterface - { - protected $authorizationChecker; - - public function __construct(AuthorizationCheckerInterface $authorizationChecker) - { - $this->authorizationChecker = $authorizationChecker; - } - - public function matches(Request $request) - { - return $this->authorizationChecker->isGranted('ROLE_SUPER_ADMIN'); - } - } - -.. versionadded:: 2.6 - The :class:`Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface` was - introduced in Symfony 2.6. Prior, you had to use the ``isGranted`` method of - :class:`Symfony\\Component\\Security\\Core\\SecurityContextInterface`. - -Then, you need to configure the service: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/services.yml - services: - app.profiler.matcher.super_admin: - class: AppBundle\Profiler\SuperAdminMatcher - arguments: ["@security.authorization_checker"] - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - $container->setDefinition('app.profiler.matcher.super_admin', new Definition( - 'AppBundle\Profiler\SuperAdminMatcher', - array(new Reference('security.authorization_checker')) - ); - -.. versionadded:: 2.6 - The ``security.authorization_checker`` service was introduced in Symfony 2.6. Prior - to Symfony 2.6, you had to use the ``isGranted()`` method of the ``security.context`` service. - -Now the service is registered, the only thing left to do is configure the -profiler to use this service as the matcher: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - # ... - profiler: - matcher: - service: app.profiler.matcher.super_admin - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - // ... - 'profiler' => array( - 'service' => 'app.profiler.matcher.super_admin', - ), - )); diff --git a/cookbook/request/index.rst b/cookbook/request/index.rst deleted file mode 100644 index 018f778e153..00000000000 --- a/cookbook/request/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Request -======= - -.. toctree:: - :maxdepth: 2 - - load_balancer_reverse_proxy - mime_type diff --git a/cookbook/request/mime_type.rst b/cookbook/request/mime_type.rst deleted file mode 100644 index 269156654c3..00000000000 --- a/cookbook/request/mime_type.rst +++ /dev/null @@ -1,118 +0,0 @@ -.. index:: - single: Request; Add a request format and mime type - -How to Register a new Request Format and Mime Type -================================================== - -Every ``Request`` has a "format" (e.g. ``html``, ``json``), which is used -to determine what type of content to return in the ``Response``. In fact, -the request format, accessible via -:method:`Symfony\\Component\\HttpFoundation\\Request::getRequestFormat`, -is used to set the MIME type of the ``Content-Type`` header on the ``Response`` -object. Internally, Symfony contains a map of the most common formats (e.g. -``html``, ``json``) and their associated MIME types (e.g. ``text/html``, -``application/json``). Of course, additional format-MIME type entries can -easily be added. This document will show how you can add the ``jsonp`` format -and corresponding MIME type. - -Configure your New Format -------------------------- - -The FrameworkBundle registers a subscriber that will add formats to incoming requests. - -All you have to do is to configure the ``jsonp`` format: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - request: - formats: - jsonp: 'application/javascript' - - .. code-block:: xml - - - - - - - - - application/javascript - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'request' => array( - 'formats' => array( - 'jsonp' => 'application/javascript', - ), - ), - )); - -.. tip:: - - You can also associate multiple mime types to a format, but please note that - the preferred one must be the first as it will be used as the content type: - - .. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - request: - formats: - csv: ['text/csv', 'text/plain'] - - .. code-block:: xml - - - - - - - - - text/csv - text/plain - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', array( - 'request' => array( - 'formats' => array( - 'jsonp' => array( - 'text/csv', - 'text/plain', - ), - ), - ), - )); diff --git a/cookbook/routing/index.rst b/cookbook/routing/index.rst deleted file mode 100644 index c42cff1748d..00000000000 --- a/cookbook/routing/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Routing -======= - -.. toctree:: - :maxdepth: 2 - - scheme - slash_in_parameter - redirect_in_config - method_parameters - service_container_parameters - custom_route_loader - redirect_trailing_slash - extra_information diff --git a/cookbook/routing/method_parameters.rst b/cookbook/routing/method_parameters.rst deleted file mode 100644 index be270240dd2..00000000000 --- a/cookbook/routing/method_parameters.rst +++ /dev/null @@ -1,92 +0,0 @@ -.. index:: - single: Routing; methods - -How to Use HTTP Methods beyond GET and POST in Routes -===================================================== - -The HTTP method of a request is one of the requirements that can be checked -when seeing if it matches a route. This is introduced in the routing chapter -of the book ":doc:`/book/routing`" with examples using GET and POST. You can -also use other HTTP verbs in this way. For example, if you have a blog post -entry then you could use the same URL path to show it, make changes to it and -delete it by matching on GET, PUT and DELETE. - -.. configuration-block:: - - .. code-block:: yaml - - blog_show: - path: /blog/{slug} - defaults: { _controller: AppBundle:Blog:show } - methods: [GET] - - blog_update: - path: /blog/{slug} - defaults: { _controller: AppBundle:Blog:update } - methods: [PUT] - - blog_delete: - path: /blog/{slug} - defaults: { _controller: AppBundle:Blog:delete } - methods: [DELETE] - - .. code-block:: xml - - - - - - - AppBundle:Blog:show - - - - AppBundle:Blog:update - - - - AppBundle:Blog:delete - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('blog_show', new Route('/blog/{slug}', array( - '_controller' => 'AppBundle:Blog:show', - ), array(), array(), '', array(), array('GET'))); - - $collection->add('blog_update', new Route('/blog/{slug}', array( - '_controller' => 'AppBundle:Blog:update', - ), array(), array(), '', array(), array('PUT'))); - - $collection->add('blog_delete', new Route('/blog/{slug}', array( - '_controller' => 'AppBundle:Blog:delete', - ), array(), array(), '', array('DELETE'))); - - return $collection; - -Faking the Method with ``_method`` ----------------------------------- - -.. note:: - - The ``_method`` functionality shown here is disabled by default in Symfony 2.2 - and enabled by default in Symfony 2.3. To control it in Symfony 2.2, you - must call :method:`Request::enableHttpMethodParameterOverride ` - before you handle the request (e.g. in your front controller). In Symfony - 2.3, use the :ref:`configuration-framework-http_method_override` option. - -Unfortunately, life isn't quite this simple, since most browsers do not support -sending PUT and DELETE requests via the `method` attribute in an HTML form. Fortunately, -Symfony provides you with a simple way of working around this limitation. By including -a ``_method`` parameter in the query string or parameters of an HTTP request, Symfony -will use this as the method when matching routes. Forms automatically include a -hidden field for this parameter if their submission method is not GET or POST. -See :ref:`the related chapter in the forms documentation` -for more information. diff --git a/cookbook/routing/slash_in_parameter.rst b/cookbook/routing/slash_in_parameter.rst deleted file mode 100644 index 9a2801705cd..00000000000 --- a/cookbook/routing/slash_in_parameter.rst +++ /dev/null @@ -1,78 +0,0 @@ -.. index:: - single: Routing; Allow / in route parameter - -How to Allow a "/" Character in a Route Parameter -================================================= - -Sometimes, you need to compose URLs with parameters that can contain a slash -``/``. For example, take the classic ``/hello/{username}`` route. By default, -``/hello/Fabien`` will match this route but not ``/hello/Fabien/Kris``. This -is because Symfony uses this character as separator between route parts. - -This guide covers how you can modify a route so that ``/hello/Fabien/Kris`` -matches the ``/hello/{username}`` route, where ``{username}`` equals ``Fabien/Kris``. - -Configure the Route -------------------- - -By default, the Symfony Routing component requires that the parameters -match the following regex path: ``[^/]+``. This means that all characters -are allowed except ``/``. - -You must explicitly allow ``/`` to be part of your parameter by specifying -a more permissive regex path. - -.. configuration-block:: - - .. code-block:: php-annotations - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - - class DemoController - { - /** - * @Route("/hello/{username}", name="_hello", requirements={"username"=".+"}) - */ - public function helloAction($username) - { - // ... - } - } - - .. code-block:: yaml - - _hello: - path: /hello/{username} - defaults: { _controller: AppBundle:Demo:hello } - requirements: - username: .+ - - .. code-block:: xml - - - - - - - AppBundle:Demo:hello - .+ - - - - .. code-block:: php - - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $collection = new RouteCollection(); - $collection->add('_hello', new Route('/hello/{username}', array( - '_controller' => 'AppBundle:Demo:hello', - ), array( - 'username' => '.+', - ))); - - return $collection; - -That's it! Now, the ``{username}`` parameter can contain the ``/`` character. diff --git a/cookbook/security/index.rst b/cookbook/security/index.rst deleted file mode 100644 index c9a478c927a..00000000000 --- a/cookbook/security/index.rst +++ /dev/null @@ -1,38 +0,0 @@ -Security -======== - -Authentication (Identifying/Logging in the User) ------------------------------------------------- - -.. toctree:: - :maxdepth: 2 - - form_login_setup - entity_provider - remember_me - impersonating_user - form_login - custom_provider - custom_password_authenticator - api_key_authentication - custom_authentication_provider - pre_authenticated - target_path - csrf_in_login_form - named_encoders - multiple_user_providers - firewall_restriction - host_restriction - -Authorization (Denying Access) ------------------------------- - -.. toctree:: - :maxdepth: 2 - - voters - acl - acl_advanced - force_https - securing_services - access_control diff --git a/cookbook/security/target_path.rst b/cookbook/security/target_path.rst deleted file mode 100644 index e9da39cf9ca..00000000000 --- a/cookbook/security/target_path.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. index:: - single: Security; Target redirect path - -How to Change the default Target Path Behavior -============================================== - -By default, the Security component retains the information of the last request -URI in a session variable named ``_security.main.target_path`` (with ``main`` being -the name of the firewall, defined in ``security.yml``). Upon a successful -login, the user is redirected to this path, as to help them continue from the -last known page they visited. - -In some situations, this is not ideal. For example, when the last request -URI was an XMLHttpRequest which returned a non-HTML or partial HTML response, -the user is redirected back to a page which the browser cannot render. - -To get around this behavior, you would simply need to extend the ``ExceptionListener`` -class and override the default method named ``setTargetPath()``. - -First, override the ``security.exception_listener.class`` parameter in your -configuration file. This can be done from your main configuration file (in -``app/config``) or from a configuration file being imported from a bundle: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/services.yml - parameters: - # ... - security.exception_listener.class: AppBundle\Security\Firewall\ExceptionListener - - .. code-block:: xml - - - - - AppBundle\Security\Firewall\ExceptionListener - - - .. code-block:: php - - // app/config/services.php - // ... - $container->setParameter('security.exception_listener.class', 'AppBundle\Security\Firewall\ExceptionListener'); - -Next, create your own ``ExceptionListener``:: - - // src/AppBundle/Security/Firewall/ExceptionListener.php - namespace AppBundle\Security\Firewall; - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Http\Firewall\ExceptionListener as BaseExceptionListener; - - class ExceptionListener extends BaseExceptionListener - { - protected function setTargetPath(Request $request) - { - // Do not save target path for XHR requests - // You can add any more logic here you want - // Note that non-GET requests are already ignored - if ($request->isXmlHttpRequest()) { - return; - } - - parent::setTargetPath($request); - } - } - -Add as much or as little logic here as required for your scenario! diff --git a/cookbook/service_container/event_listener.rst b/cookbook/service_container/event_listener.rst deleted file mode 100644 index 3b9c94373ae..00000000000 --- a/cookbook/service_container/event_listener.rst +++ /dev/null @@ -1,153 +0,0 @@ -.. index:: - single: Events; Create listener - -How to Create an Event Listener -=============================== - -Symfony has various events and hooks that can be used to trigger custom -behavior in your application. Those events are thrown by the HttpKernel -component and can be viewed in the :class:`Symfony\\Component\\HttpKernel\\KernelEvents` class. - -To hook into an event and add your own custom logic, you have to create -a service that will act as an event listener on that event. In this entry, -you will create a service that will act as an exception listener, allowing -you to modify how exceptions are shown by your application. The ``KernelEvents::EXCEPTION`` -event is just one of the core kernel events:: - - // src/AppBundle/EventListener/AcmeExceptionListener.php - namespace AppBundle\EventListener; - - use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; - - class AcmeExceptionListener - { - public function onKernelException(GetResponseForExceptionEvent $event) - { - // You get the exception object from the received event - $exception = $event->getException(); - $message = sprintf( - 'My Error says: %s with code: %s', - $exception->getMessage(), - $exception->getCode() - ); - - // Customize your response object to display the exception details - $response = new Response(); - $response->setContent($message); - - // HttpExceptionInterface is a special type of exception that - // holds status code and header details - if ($exception instanceof HttpExceptionInterface) { - $response->setStatusCode($exception->getStatusCode()); - $response->headers->replace($exception->getHeaders()); - } else { - $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); - } - - // Send the modified response object to the event - $event->setResponse($response); - } - } - -.. tip:: - - Each event receives a slightly different type of ``$event`` object. For - the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`. - To see what type of object each event listener receives, see :class:`Symfony\\Component\\HttpKernel\\KernelEvents`. - -.. note:: - - When setting a response for the ``kernel.request``, ``kernel.view`` or - ``kernel.exception`` events, the propagation is stopped, so the lower - priority listeners on that event don't get called. - -Now that the class is created, you just need to register it as a service and -notify Symfony that it is a "listener" on the ``kernel.exception`` event by -using a special "tag": - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/services.yml - services: - kernel.listener.your_listener_name: - class: AppBundle\EventListener\AcmeExceptionListener - tags: - - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } - - .. code-block:: xml - - - - - - - .. code-block:: php - - // app/config/services.php - $container - ->register('kernel.listener.your_listener_name', 'AppBundle\EventListener\AcmeExceptionListener') - ->addTag('kernel.event_listener', array('event' => 'kernel.exception', 'method' => 'onKernelException')) - ; - -.. note:: - - There is an additional tag option ``priority`` that is optional and defaults - to 0. The listeners will be executed in the order of their priority (highest to lowest). - This is useful when you need to guarantee that one listener is executed before another. - -Request Events, Checking Types ------------------------------- - -A single page can make several requests (one master request, and then multiple -sub-requests), which is why when working with the ``KernelEvents::REQUEST`` -event, you might need to check the type of the request. This can be easily -done as follow:: - - // src/AppBundle/EventListener/AcmeRequestListener.php - namespace AppBundle\EventListener; - - use Symfony\Component\HttpKernel\Event\GetResponseEvent; - use Symfony\Component\HttpKernel\HttpKernel; - - class AcmeRequestListener - { - public function onKernelRequest(GetResponseEvent $event) - { - if (!$event->isMasterRequest()) { - // don't do anything if it's not the master request - return; - } - - // ... - } - } - -.. tip:: - - Two types of request are available in the :class:`Symfony\\Component\\HttpKernel\\HttpKernelInterface` - interface: ``HttpKernelInterface::MASTER_REQUEST`` and - ``HttpKernelInterface::SUB_REQUEST``. - -Debugging Event Listeners -------------------------- - -.. versionadded:: 2.6 - The ``debug:event-dispatcher`` command was introduced in Symfony 2.6. - -You can find out what listeners are registered in the event dispatcher -using the console. To show all events and their listeners, run: - -.. code-block:: bash - - $ php app/console debug:event-dispatcher - -You can get registered listeners for a particular event by specifying -its name: - -.. code-block:: bash - - $ php app/console debug:event-dispatcher kernel.exception diff --git a/cookbook/service_container/index.rst b/cookbook/service_container/index.rst deleted file mode 100644 index be8ad17868b..00000000000 --- a/cookbook/service_container/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Service Container -================= - -.. toctree:: - :maxdepth: 2 - - event_listener - scopes - compiler_passes diff --git a/cookbook/session/index.rst b/cookbook/session/index.rst deleted file mode 100644 index a3490ec0b39..00000000000 --- a/cookbook/session/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Sessions -======== - -.. toctree:: - :maxdepth: 2 - - proxy_examples - locale_sticky_session - sessions_directory - php_bridge - limit_metadata_writes - avoid_session_start diff --git a/cookbook/session/proxy_examples.rst b/cookbook/session/proxy_examples.rst deleted file mode 100644 index 34c28d78f99..00000000000 --- a/cookbook/session/proxy_examples.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. index:: - single: Sessions, Session Proxy, Proxy - -Session Proxy Examples -====================== - -The session proxy mechanism has a variety of uses and this example demonstrates -two common uses. Rather than injecting the session handler as normal, a handler -is injected into the proxy and registered with the session storage driver:: - - use Symfony\Component\HttpFoundation\Session\Session; - use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; - use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; - - $proxy = new YourProxy(new PdoSessionHandler()); - $session = new Session(new NativeSessionStorage(array(), $proxy)); - -Below, you'll learn two real examples that can be used for ``YourProxy``: -encryption of session data and readonly guest sessions. - -Encryption of Session Data --------------------------- - -If you wanted to encrypt the session data, you could use the proxy to encrypt -and decrypt the session as required:: - - use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; - - class EncryptedSessionProxy extends SessionHandlerProxy - { - private $key; - - public function __construct(\SessionHandlerInterface $handler, $key) - { - $this->key = $key; - - parent::__construct($handler); - } - - public function read($id) - { - $data = parent::read($id); - - return mcrypt_decrypt(\MCRYPT_3DES, $this->key, $data); - } - - public function write($id, $data) - { - $data = mcrypt_encrypt(\MCRYPT_3DES, $this->key, $data); - - return parent::write($id, $data); - } - } - -Readonly Guest Sessions ------------------------ - -There are some applications where a session is required for guest users, but -where there is no particular need to persist the session. In this case you -can intercept the session before it is written:: - - use Foo\User; - use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; - - class ReadOnlyGuestSessionProxy extends SessionHandlerProxy - { - private $user; - - public function __construct(\SessionHandlerInterface $handler, User $user) - { - $this->user = $user; - - parent::__construct($handler); - } - - public function write($id, $data) - { - if ($this->user->isGuest()) { - return; - } - - return parent::write($id, $data); - } - } diff --git a/cookbook/templating/index.rst b/cookbook/templating/index.rst deleted file mode 100644 index 46d6fe37012..00000000000 --- a/cookbook/templating/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Templating -========== - -.. toctree:: - :maxdepth: 2 - - global_variables - namespaced_paths - PHP - twig_extension - render_without_controller diff --git a/cookbook/testing/http_authentication.rst b/cookbook/testing/http_authentication.rst deleted file mode 100644 index c201562a017..00000000000 --- a/cookbook/testing/http_authentication.rst +++ /dev/null @@ -1,56 +0,0 @@ -.. index:: - single: Tests; HTTP authentication - -How to Simulate HTTP Authentication in a Functional Test -======================================================== - -If your application needs HTTP authentication, pass the username and password -as server variables to ``createClient()``:: - - $client = static::createClient(array(), array( - 'PHP_AUTH_USER' => 'username', - 'PHP_AUTH_PW' => 'pa$$word', - )); - -You can also override it on a per request basis:: - - $client->request('DELETE', '/post/12', array(), array(), array( - 'PHP_AUTH_USER' => 'username', - 'PHP_AUTH_PW' => 'pa$$word', - )); - -When your application is using a ``form_login``, you can simplify your tests -by allowing your test configuration to make use of HTTP authentication. This -way you can use the above to authenticate in tests, but still have your users -log in via the normal ``form_login``. The trick is to include the ``http_basic`` -key in your firewall, along with the ``form_login`` key: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config_test.yml - security: - firewalls: - your_firewall_name: - http_basic: ~ - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/config_test.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'your_firewall_name' => array( - 'http_basic' => array(), - ), - ), - )); diff --git a/cookbook/testing/index.rst b/cookbook/testing/index.rst deleted file mode 100644 index 49967eaeee1..00000000000 --- a/cookbook/testing/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Testing -======= - -.. toctree:: - :maxdepth: 2 - - http_authentication - simulating_authentication - insulating_clients - profiling - database - doctrine - bootstrap diff --git a/cookbook/testing/simulating_authentication.rst b/cookbook/testing/simulating_authentication.rst deleted file mode 100644 index f2e04acd612..00000000000 --- a/cookbook/testing/simulating_authentication.rst +++ /dev/null @@ -1,61 +0,0 @@ -.. index:: - single: Tests; Simulating authentication - -How to Simulate Authentication with a Token in a Functional Test -================================================================ - -Authenticating requests in functional tests might slow down the suite. -It could become an issue especially when ``form_login`` is used, since -it requires additional requests to fill in and submit the form. - -One of the solutions is to configure your firewall to use ``http_basic`` in -the test environment as explained in -:doc:`/cookbook/testing/http_authentication`. -Another way would be to create a token yourself and store it in a session. -While doing this, you have to make sure that an appropriate cookie is sent -with a request. The following example demonstrates this technique:: - - // src/AppBundle/Tests/Controller/DefaultControllerTest.php - namespace Appbundle\Tests\Controller; - - use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - use Symfony\Component\BrowserKit\Cookie; - use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; - - class DefaultControllerTest extends WebTestCase - { - private $client = null; - - public function setUp() - { - $this->client = static::createClient(); - } - - public function testSecuredHello() - { - $this->logIn(); - - $crawler = $this->client->request('GET', '/admin'); - - $this->assertTrue($this->client->getResponse()->isSuccessful()); - $this->assertGreaterThan(0, $crawler->filter('html:contains("Admin Dashboard")')->count()); - } - - private function logIn() - { - $session = $this->client->getContainer()->get('session'); - - $firewall = 'secured_area'; - $token = new UsernamePasswordToken('admin', null, $firewall, array('ROLE_ADMIN')); - $session->set('_security_'.$firewall, serialize($token)); - $session->save(); - - $cookie = new Cookie($session->getName(), $session->getId()); - $this->client->getCookieJar()->set($cookie); - } - } - -.. note:: - - The technique described in :doc:`/cookbook/testing/http_authentication` - is cleaner and therefore the preferred way. diff --git a/cookbook/upgrade/index.rst b/cookbook/upgrade/index.rst deleted file mode 100644 index b943f2ae32a..00000000000 --- a/cookbook/upgrade/index.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. index:: - single: Upgrading - -Upgrading -========= - -So a new Symfony release has come out and you want to upgrade, great! Fortunately, -because Symfony protects backwards-compatibility very closely, this *should* -be quite easy. - -There are three types of upgrades, all needing a little different preparation: - -.. toctree:: - :maxdepth: 2 - - /cookbook/upgrade/patch_version - /cookbook/upgrade/minor_version - /cookbook/upgrade/major_version diff --git a/cookbook/validation/index.rst b/cookbook/validation/index.rst deleted file mode 100644 index d194c118025..00000000000 --- a/cookbook/validation/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Validation -========== - -.. toctree:: - :maxdepth: 2 - - custom_constraint - severity diff --git a/cookbook/web_server/index.rst b/cookbook/web_server/index.rst deleted file mode 100644 index 4b956308fc0..00000000000 --- a/cookbook/web_server/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Web Server -========== - -.. toctree:: - :maxdepth: 2 - - built_in diff --git a/cookbook/web_services/index.rst b/cookbook/web_services/index.rst deleted file mode 100644 index e92c057cc4a..00000000000 --- a/cookbook/web_services/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Web Services -============ - -.. toctree:: - :maxdepth: 2 - - php_soap_extension diff --git a/cookbook/workflow/index.rst b/cookbook/workflow/index.rst deleted file mode 100644 index 6b2382ae235..00000000000 --- a/cookbook/workflow/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Workflow -======== - -.. toctree:: - :maxdepth: 2 - - new_project_git - new_project_svn diff --git a/create_framework/dependency-injection.rst b/create_framework/dependency_injection.rst similarity index 80% rename from create_framework/dependency-injection.rst rename to create_framework/dependency_injection.rst index 4187f669937..9833da00451 100644 --- a/create_framework/dependency-injection.rst +++ b/create_framework/dependency_injection.rst @@ -7,7 +7,6 @@ empty class, you might be tempted to move some code from the front controller to it:: // example.com/src/Simplex/Framework.php - namespace Simplex; use Symfony\Component\Routing; @@ -33,7 +32,6 @@ to it:: The front controller code would become more concise:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -87,43 +85,46 @@ front controller? As you might expect, there is a solution. We can solve all these issues and some more by using the Symfony dependency injection container: -.. code-block:: bash +.. code-block:: terminal $ composer require symfony/dependency-injection Create a new file to host the dependency injection container configuration:: // example.com/src/container.php - use Symfony\Component\DependencyInjection; use Symfony\Component\DependencyInjection\Reference; + use Symfony\Component\HttpKernel; + use Symfony\Component\Routing; + use Symfony\Component\EventDispatcher; + use Simplex\Framework; - $sc = new DependencyInjection\ContainerBuilder(); - $sc->register('context', 'Symfony\Component\Routing\RequestContext'); - $sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher') + $containerBuilder = new DependencyInjection\ContainerBuilder(); + $containerBuilder->register('context', Routing\RequestContext::class); + $containerBuilder->register('matcher', Routing\Matcher\UrlMatcher::class) ->setArguments(array($routes, new Reference('context'))) ; - $sc->register('resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver'); + $containerBuilder->register('resolver', HttpKernel\Controller\ControllerResolver::class); - $sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener') + $containerBuilder->register('listener.router', HttpKernel\EventListener\RouterListener::class) ->setArguments(array(new Reference('matcher'))) ; - $sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener') + $containerBuilder->register('listener.response', HttpKernel\EventListener\ResponseListener::class) ->setArguments(array('UTF-8')) ; - $sc->register('listener.exception', 'Symfony\Component\HttpKernel\EventListener\ExceptionListener') - ->setArguments(array('Calendar\\Controller\\ErrorController::exceptionAction')) + $containerBuilder->register('listener.exception', HttpKernel\EventListener\ExceptionListener::class) + ->setArguments(array('Calendar\Controller\ErrorController::exceptionAction')) ; - $sc->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher') + $containerBuilder->register('dispatcher', EventDispatcher\EventDispatcher::class) ->addMethodCall('addSubscriber', array(new Reference('listener.router'))) ->addMethodCall('addSubscriber', array(new Reference('listener.response'))) ->addMethodCall('addSubscriber', array(new Reference('listener.exception'))) ; - $sc->register('framework', 'Simplex\Framework') + $containerBuilder->register('framework', Framework::class) ->setArguments(array(new Reference('dispatcher'), new Reference('resolver'))) ; - return $sc; + return $containerBuilder; The goal of this file is to configure your objects and their dependencies. Nothing is instantiated during this configuration step. This is purely a @@ -132,7 +133,7 @@ them. Objects will be created on-demand when you access them from the container or when the container needs them to create other objects. For instance, to create the router listener, we tell Symfony that its class -name is ``Symfony\Component\HttpKernel\EventListener\RouterListener``, and +name is ``Symfony\Component\HttpKernel\EventListener\RouterListener`` and that its constructor takes a matcher object (``new Reference('matcher')``). As you can see, each object is referenced by a name, a string that uniquely identifies each object. The name allows us to get an object and to reference @@ -147,17 +148,16 @@ it in other object definitions. The front controller is now only about wiring everything together:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; $routes = include __DIR__.'/../src/app.php'; - $sc = include __DIR__.'/../src/container.php'; + $container = include __DIR__.'/../src/container.php'; $request = Request::createFromGlobals(); - $response = $sc->get('framework')->handle($request); + $response = $container->get('framework')->handle($request); $response->send(); @@ -165,7 +165,6 @@ As all the objects are now created in the dependency injection container, the framework code should be the previous simple version:: // example.com/src/Simplex/Framework.php - namespace Simplex; use Symfony\Component\HttpKernel\HttpKernel; @@ -181,8 +180,11 @@ framework code should be the previous simple version:: Now, here is how you can register a custom listener in the front controller:: - $sc->register('listener.string_response', 'Simplex\StringResponseListener'); - $sc->getDefinition('dispatcher') + // ... + use Simplex\StringResponseListener; + + $container->register('listener.string_response', StringResponseListener::class); + $container->getDefinition('dispatcher') ->addMethodCall('addSubscriber', array(new Reference('listener.string_response'))) ; @@ -190,32 +192,34 @@ Beside describing your objects, the dependency injection container can also be configured via parameters. Let's create one that defines if we are in debug mode or not:: - $sc->setParameter('debug', true); + $container->setParameter('debug', true); - echo $sc->getParameter('debug'); + echo $container->getParameter('debug'); These parameters can be used when defining object definitions. Let's make the charset configurable:: - $sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener') + // ... + $container->register('listener.response', HttpKernel\EventListener\ResponseListener::class) ->setArguments(array('%charset%')) ; After this change, you must set the charset before using the response listener object:: - $sc->setParameter('charset', 'UTF-8'); + $container->setParameter('charset', 'UTF-8'); Instead of relying on the convention that the routes are defined by the ``$routes`` variables, let's use a parameter again:: - $sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher') + // ... + $container->register('matcher', Routing\Matcher\UrlMatcher::class) ->setArguments(array('%routes%', new Reference('context'))) ; And the related change in the front controller:: - $sc->setParameter('routes', include __DIR__.'/../src/app.php'); + $container->setParameter('routes', include __DIR__.'/../src/app.php'); We have obviously barely scratched the surface of what you can do with the container: from class names as parameters, to overriding existing object @@ -238,6 +242,6 @@ micro-framework, and especially its `Application`_ class. Have fun! -.. _`Pimple`: https://github.com/fabpot/Pimple -.. _`Silex`: https://silex.sensiolabs.org/ -.. _`Application`: https://github.com/fabpot/Silex/blob/master/src/Silex/Application.php +.. _`Pimple`: https://github.com/silexphp/Pimple +.. _`Silex`: http://silex.sensiolabs.org/ +.. _`Application`: https://github.com/silexphp/Silex/blob/master/src/Silex/Application.php diff --git a/create_framework/event-dispatcher.rst b/create_framework/event_dispatcher.rst similarity index 96% rename from create_framework/event-dispatcher.rst rename to create_framework/event_dispatcher.rst index 9d6b8c1ba89..f740a8d39a6 100644 --- a/create_framework/event-dispatcher.rst +++ b/create_framework/event_dispatcher.rst @@ -17,7 +17,7 @@ pattern, the *Mediator*, to allow any kind of behaviors to be attached to our framework; the Symfony EventDispatcher Component implements a lightweight version of this pattern: -.. code-block:: bash +.. code-block:: terminal $ composer require symfony/event-dispatcher @@ -34,7 +34,6 @@ To make it work, the framework must dispatch an event just before returning the Response instance:: // example.com/src/Simplex/Framework.php - namespace Simplex; use Symfony\Component\HttpFoundation\Request; @@ -46,9 +45,9 @@ the Response instance:: class Framework { - protected $matcher; - protected $resolver; - protected $dispatcher; + private $matcher; + private $resolver; + private $dispatcher; public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $resolver) { @@ -68,9 +67,9 @@ the Response instance:: $arguments = $this->resolver->getArguments($request, $controller); $response = call_user_func_array($controller, $arguments); - } catch (ResourceNotFoundException $e) { + } catch (ResourceNotFoundException $exception) { $response = new Response('Not Found', 404); - } catch (\Exception $e) { + } catch (\Exception $exception) { $response = new Response('An error occurred', 500); } @@ -85,7 +84,6 @@ Each time the framework handles a Request, a ``ResponseEvent`` event is now dispatched:: // example.com/src/Simplex/ResponseEvent.php - namespace Simplex; use Symfony\Component\HttpFoundation\Request; @@ -118,7 +116,6 @@ The last step is the creation of the dispatcher in the front controller and the registration of a listener for the ``response`` event:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; // ... @@ -154,7 +151,7 @@ event (``response``); the event name must be the same as the one used in the ``dispatch()`` call. In the listener, we add the Google Analytics code only if the response is not -a redirection, if the requested format is HTML, and if the response content +a redirection, if the requested format is HTML and if the response content type is HTML (these conditions demonstrate the ease of manipulating the Request and Response data from your code). @@ -197,7 +194,6 @@ the priority to ``-255``:: Let's refactor the code a bit by moving the Google listener to its own class:: // example.com/src/Simplex/GoogleListener.php - namespace Simplex; class GoogleListener @@ -220,7 +216,6 @@ Let's refactor the code a bit by moving the Google listener to its own class:: And do the same with the other listener:: // example.com/src/Simplex/ContentLengthListener.php - namespace Simplex; class ContentLengthListener @@ -259,7 +254,6 @@ information to the dispatcher via the ``getSubscribedEvents()`` method. Have a look at the new version of the ``GoogleListener``:: // example.com/src/Simplex/GoogleListener.php - namespace Simplex; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -277,7 +271,6 @@ look at the new version of the ``GoogleListener``:: And here is the new version of ``ContentLengthListener``:: // example.com/src/Simplex/ContentLengthListener.php - namespace Simplex; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -302,5 +295,5 @@ to make it more awesome out of the box, add more listeners. Again, this book is not about creating a generic framework, but one that is tailored to your needs. Stop whenever you see fit, and further evolve the code from there. -.. _`WSGI`: http://www.python.org/dev/peps/pep-0333/#middleware-components-that-play-both-sides +.. _`WSGI`: https://www.python.org/dev/peps/pep-0333/#middleware-components-that-play-both-sides .. _`Rack`: http://rack.rubyforge.org/ diff --git a/create_framework/front-controller.rst b/create_framework/front_controller.rst similarity index 92% rename from create_framework/front-controller.rst rename to create_framework/front_controller.rst index c132580f9c4..8698865aa46 100644 --- a/create_framework/front-controller.rst +++ b/create_framework/front_controller.rst @@ -6,7 +6,6 @@ spice things up a little bit, let's go crazy and add another page that says goodbye:: // framework/bye.php - require_once __DIR__.'/vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -26,7 +25,6 @@ The PHP way of doing the refactoring would probably be the creation of an include file:: // framework/init.php - require_once __DIR__.'/vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -38,18 +36,16 @@ include file:: Let's see it in action:: // framework/index.php - require_once __DIR__.'/init.php'; - $input = $request->get('name', 'World'); + $name = $request->get('name', 'World'); - $response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); + $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); $response->send(); And for the "Goodbye" page:: // framework/bye.php - require_once __DIR__.'/init.php'; $response->setContent('Goodbye!'); @@ -57,7 +53,7 @@ And for the "Goodbye" page:: We have indeed moved most of the shared code into a central place, but it does not feel like a good abstraction, does it? We still have the ``send()`` method -for all pages, our pages do not look like templates, and we are still not able +for all pages, our pages do not look like templates and we are still not able to test this code properly. Moreover, adding a new page means that we need to create a new PHP script, @@ -71,12 +67,11 @@ routing all client requests to a single PHP script. .. tip:: Exposing a single PHP script to the end user is a design pattern called - the "`front controller`_". + the ":ref:`front controller `". Such a script might look like the following:: // framework/front.php - require_once __DIR__.'/vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -103,9 +98,8 @@ Such a script might look like the following:: And here is for instance the new ``hello.php`` script:: // framework/hello.php - - $input = $request->get('name', 'World'); - $response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); + $name = $request->get('name', 'World'); + $response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); In the ``front.php`` script, ``$map`` associates URL paths with their corresponding PHP script paths. @@ -146,7 +140,7 @@ web root directory: example.com ├── composer.json - ├── composer.lock + ├── composer.lock ├── src │ └── pages │ ├── hello.php @@ -159,10 +153,10 @@ web root directory: Now, configure your web server root directory to point to ``web/`` and all other files won't be accessible from the client anymore. -To test your changes in a browser (``http://localhost:4321/?name=Fabien``), run +To test your changes in a browser (``http://localhost:4321/hello/?name=Fabien``), run the PHP built-in server: -.. code-block:: bash +.. code-block:: terminal $ php -S 127.0.0.1:4321 -t web/ web/front.php @@ -194,7 +188,6 @@ the ``setContent()`` directly from the front controller script:: And the ``hello.php`` script can now be converted to a template:: - get('name', 'World') ?> Hello @@ -202,7 +195,6 @@ And the ``hello.php`` script can now be converted to a template:: We have the first version of our framework:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -237,5 +229,3 @@ variable. If you decide to stop here, you can probably enhance your framework by extracting the URL map to a configuration file. - -.. _`front controller`: http://symfony.com/doc/current/book/from_flat_php_to_symfony2.html#a-front-controller-to-the-rescue diff --git a/create_framework/http-foundation.rst b/create_framework/http_foundation.rst similarity index 83% rename from create_framework/http-foundation.rst rename to create_framework/http_foundation.rst index e7d4c41646b..a48cbfebaa3 100644 --- a/create_framework/http-foundation.rst +++ b/create_framework/http_foundation.rst @@ -18,29 +18,27 @@ Even if the "application" we wrote in the previous chapter was simple enough, it suffers from a few problems:: // framework/index.php + $name = $_GET['name']; - $input = $_GET['name']; - - printf('Hello %s', $input); + printf('Hello %s', $name); First, if the ``name`` query parameter is not defined in the URL query string, you will get a PHP warning; so let's fix it:: // framework/index.php + $name = isset($_GET['name']) ? $_GET['name'] : 'World'; - $input = isset($_GET['name']) ? $_GET['name'] : 'World'; - - printf('Hello %s', $input); + printf('Hello %s', $name); Then, this *application is not secure*. Can you believe it? Even this simple snippet of PHP code is vulnerable to one of the most widespread Internet security issue, XSS (Cross-Site Scripting). Here is a more secure version:: - $input = isset($_GET['name']) ? $_GET['name'] : 'World'; + $name = isset($_GET['name']) ? $_GET['name'] : 'World'; header('Content-Type: text/html; charset=utf-8'); - printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')); + printf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')); .. note:: @@ -60,8 +58,9 @@ snippet of PHP code is not natural and feels ugly. Here is a tentative PHPUnit unit test for the above code:: // framework/test.php + use PHPUnit\Framework\TestCase; - class IndexTest extends \PHPUnit_Framework_TestCase + class IndexTest extends TestCase { public function testHello() { @@ -78,8 +77,8 @@ unit test for the above code:: .. note:: If our application were just slightly bigger, we would have been able to - find even more problems. If you are curious about them, read the `Symfony - versus Flat PHP`_ chapter of the Symfony documentation. + find even more problems. If you are curious about them, read the + :doc:`/introduction/from_flat_php_to_symfony2` chapter of the book. At this point, if you are not convinced that security and testing are indeed two very good reasons to stop writing code the old way and adopt a framework @@ -115,25 +114,14 @@ layer. To use this component, add it as a dependency of the project: -.. code-block:: bash +.. code-block:: terminal $ composer require symfony/http-foundation Running this command will also automatically download the Symfony HttpFoundation component and install it under the ``vendor/`` directory. A ``composer.json`` and a ``composer.lock`` file will be generated as well, -containing the new requirement: - -.. code-block:: json - - { - "require": { - "symfony/http-foundation": "^2.7" - } - } - -The code block shows the content of the ``composer.json`` file (the actual -version may vary). +containing the new requirement. .. sidebar:: Class Autoloading @@ -141,13 +129,12 @@ version may vary). ``vendor/autoload.php`` file that allows any class to be easily `autoloaded`_. Without autoloading, you would need to require the file where a class is defined before being able to use it. But thanks to - `PSR-0`_, we can just let Composer and PHP do the hard work for us. + `PSR-4`_, we can just let Composer and PHP do the hard work for us. Now, let's rewrite our application by using the ``Request`` and the ``Response`` classes:: // framework/index.php - require_once __DIR__.'/vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -155,9 +142,9 @@ Now, let's rewrite our application by using the ``Request`` and the $request = Request::createFromGlobals(); - $input = $request->get('name', 'World'); + $name = $request->get('name', 'World'); - $response = new Response(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); + $response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'))); $response->send(); @@ -227,10 +214,10 @@ With the ``Response`` class, you can easily tweak the response:: .. tip:: - To debug a Response, cast it to a string; it will return the HTTP + To debug a response, cast it to a string; it will return the HTTP representation of the response (headers and content). -Last but not the least, these classes, like every other class in the Symfony +Last but not least, these classes, like every other class in the Symfony code, have been `audited`_ for security issues by an independent company. And being an Open-Source project also means that many other developers around the world have read the code and have already fixed potential security problems. @@ -239,7 +226,7 @@ framework? Even something as simple as getting the client IP address can be insecure:: - if ($myIp == $_SERVER['REMOTE_ADDR']) { + if ($myIp === $_SERVER['REMOTE_ADDR']) { // the client is a known one, so give it some more privilege } @@ -248,7 +235,7 @@ production servers; at this point, you will have to change your code to make it work on both your development machine (where you don't have a proxy) and your servers:: - if ($myIp == $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp == $_SERVER['REMOTE_ADDR']) { + if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp === $_SERVER['REMOTE_ADDR']) { // the client is a known one, so give it some more privilege } @@ -258,7 +245,7 @@ chained proxies):: $request = Request::createFromGlobals(); - if ($myIp == $request->getClientIp()) { + if ($myIp === $request->getClientIp()) { // the client is a known one, so give it some more privilege } @@ -271,7 +258,7 @@ explicitly trust your reverse proxies by calling ``setTrustedProxies()``:: Request::setTrustedProxies(array('10.0.0.1')); - if ($myIp == $request->getClientIp(true)) { + if ($myIp === $request->getClientIp()) { // the client is a known one, so give it some more privilege } @@ -285,7 +272,7 @@ cases by yourself. Why not using a technology that already works? If you want to learn more about the HttpFoundation component, you can have a look at the :namespace:`Symfony\\Component\\HttpFoundation` API or read - its dedicated :doc:`documentation `. + its dedicated :doc:`documentation `. Believe or not but we have our first framework. You can stop now if you want. Using just the Symfony HttpFoundation component already allows you to write @@ -298,21 +285,20 @@ the wheel. I've almost forgot to talk about one added benefit: using the HttpFoundation component is the start of better interoperability between all frameworks and -applications using it (like `Symfony`_, `Drupal 8`_, `phpBB 4`_, `ezPublish -5`_, `Laravel`_, `Silex`_, and `more`_). - -.. _`Twig`: http://twig.sensiolabs.com/ -.. _`Symfony versus Flat PHP`: http://symfony.com/doc/current/book/from_flat_php_to_symfony2.html -.. _`HTTP specification`: http://tools.ietf.org/wg/httpbis/ -.. _`audited`: http://symfony.com/blog/symfony2-security-audit -.. _`Symfony`: http://symfony.com/ -.. _`Drupal 8`: http://drupal.org/ -.. _`phpBB 4`: http://www.phpbb.com/ -.. _`ezPublish 5`: http://ez.no/ -.. _`Laravel`: http://laravel.com/ -.. _`Silex`: http://silex.sensiolabs.org/ +applications using it (like `Symfony`_, `Drupal 8`_, `phpBB 3`_, `ezPublish +5`_, `Laravel`_, `Silex`_ and `more`_). + +.. _`Twig`: http://twig.sensiolabs.org/ +.. _`HTTP specification`: https://tools.ietf.org/wg/httpbis/ +.. _`audited`: https://symfony.com/blog/symfony2-security-audit +.. _`Symfony`: https://symfony.com/ +.. _`Drupal 8`: https://drupal.org/ +.. _`phpBB 3`: https://www.phpbb.com/ +.. _`ezPublish 5`: https://ez.no/ +.. _`Laravel`: https://laravel.com/ +.. _`Silex`: https://silex.sensiolabs.org/ .. _`Midgard CMS`: http://www.midgard-project.org/ -.. _`Zikula`: http://zikula.org/ -.. _`autoloaded`: http://php.net/autoload -.. _`PSR-0`: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md -.. _`more`: http://symfony.com/components/HttpFoundation +.. _`Zikula`: https://zikula.org/ +.. _`autoloaded`: https://php.net/autoload +.. _`PSR-4`: https://www.php-fig.org/psr/psr-4/ +.. _`more`: https://symfony.com/components/HttpFoundation diff --git a/create_framework/http-kernel-controller-resolver.rst b/create_framework/http_kernel_controller_resolver.rst similarity index 92% rename from create_framework/http-kernel-controller-resolver.rst rename to create_framework/http_kernel_controller_resolver.rst index b0393b4cdd3..8c0e5a041cb 100644 --- a/create_framework/http-kernel-controller-resolver.rst +++ b/create_framework/http_kernel_controller_resolver.rst @@ -39,7 +39,7 @@ instantiated. To solve this issue, and a bunch more, let's install and use the HttpKernel component: -.. code-block:: bash +.. code-block:: terminal $ composer require symfony/http-kernel @@ -50,6 +50,7 @@ a Request object. All controller resolvers implement the following interface:: namespace Symfony\Component\HttpKernel\Controller; + // ... interface ControllerResolverInterface { function getController(Request $request); @@ -91,7 +92,7 @@ introspects the controller signature to determine which arguments to pass to it by using the native PHP `reflection`_. The ``indexAction()`` method needs the Request object as an argument. -```getArguments()`` knows when to inject it properly if it is type-hinted +``getArguments()`` knows when to inject it properly if it is type-hinted correctly:: public function indexAction(Request $request) @@ -142,7 +143,8 @@ method is not defined, an argument has no matching attribute, ...). With the great flexibility of the default controller resolver, you might wonder why someone would want to create another one (why would there be an interface if not?). Two examples: in Symfony, ``getController()`` is - enhanced to support `controllers as services`_; and in + enhanced to support + :doc:`controllers as services `; and in `FrameworkExtraBundle`_, ``getArguments()`` is enhanced to support parameter converters, where request attributes are converted to objects automatically. @@ -150,7 +152,6 @@ method is not defined, an argument has no matching attribute, ...). Let's conclude with the new version of our framework:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -160,7 +161,7 @@ Let's conclude with the new version of our framework:: function render_template(Request $request) { - extract($request->attributes->all()); + extract($request->attributes->all(), EXTR_SKIP); ob_start(); include sprintf(__DIR__.'/../src/pages/%s.php', $_route); @@ -182,9 +183,9 @@ Let's conclude with the new version of our framework:: $arguments = $resolver->getArguments($request, $controller); $response = call_user_func_array($controller, $arguments); - } catch (Routing\Exception\ResourceNotFoundException $e) { + } catch (Routing\Exception\ResourceNotFoundException $exception) { $response = new Response('Not Found', 404); - } catch (Exception $e) { + } catch (Exception $exception) { $response = new Response('An error occurred', 500); } @@ -193,6 +194,5 @@ Let's conclude with the new version of our framework:: Think about it once more: our framework is more robust and more flexible than ever and it still has less than 40 lines of code. -.. _`reflection`: http://php.net/reflection -.. _`FrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html -.. _`controllers as services`: http://symfony.com/doc/current/cookbook/controller/service.html +.. _`reflection`: https://php.net/reflection +.. _`FrameworkExtraBundle`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html diff --git a/create_framework/http-kernel-httpkernel-class.rst b/create_framework/http_kernel_httpkernel_class.rst similarity index 90% rename from create_framework/http-kernel-httpkernel-class.rst rename to create_framework/http_kernel_httpkernel_class.rst index b66f11e0ee7..3f340b7bf57 100644 --- a/create_framework/http-kernel-httpkernel-class.rst +++ b/create_framework/http_kernel_httpkernel_class.rst @@ -11,7 +11,7 @@ There should be an easier way, right? Enter the ``HttpKernel`` class. Instead of solving the same problem over and over again and instead of reinventing the wheel each time, the ``HttpKernel`` -class is a generic, extensible, and flexible implementation of +class is a generic, extensible and flexible implementation of ``HttpKernelInterface``. This class is very similar to the framework class we have written so far: it @@ -23,7 +23,6 @@ feedback when a problem arises. Here is the new framework code:: // example.com/src/Simplex/Framework.php - namespace Simplex; use Symfony\Component\HttpKernel\HttpKernel; @@ -35,7 +34,6 @@ Here is the new framework code:: And the new front controller:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -67,7 +65,7 @@ Our code is now much more concise and surprisingly more robust and more powerful than ever. For instance, use the built-in ``ExceptionListener`` to make your error management configurable:: - $errorHandler = function (HttpKernel\Exception\FlattenException $exception) { + $errorHandler = function (Symfony\Component\Debug\Exception\FlattenException $exception) { $msg = 'Something went wrong! ('.$exception->getMessage().')'; return new Response($msg, $exception->getStatusCode()); @@ -79,17 +77,18 @@ thrown ``Exception`` instance to ease exception manipulation and display. It can take any valid controller as an exception handler, so you can create an ErrorController class instead of using a Closure:: - $listener = new HttpKernel\EventListener\ExceptionListener('Calendar\\Controller\\ErrorController::exceptionAction'); + $listener = new HttpKernel\EventListener\ExceptionListener( + 'Calendar\Controller\ErrorController::exceptionAction' + ); $dispatcher->addSubscriber($listener); The error controller reads as follows:: // example.com/src/Calendar/Controller/ErrorController.php - namespace Calendar\Controller; use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpKernel\Exception\FlattenException; + use Symfony\Component\Debug\Exception\FlattenException; class ErrorController { @@ -122,9 +121,9 @@ And in your controller, return a ``StreamedResponse`` instance instead of a .. tip:: - Read the `Internals`_ chapter of the Symfony documentation to learn more - about the events dispatched by HttpKernel and how they allow you to change - the flow of a request. + Read the :doc:`/reference/events` reference to learn more about the events + dispatched by HttpKernel and how they allow you to change the flow of a + request. Now, let's create a listener, one that allows a controller to return a string instead of a full Response object:: @@ -133,8 +132,8 @@ instead of a full Response object:: { public function indexAction(Request $request, $year) { - $leapyear = new LeapYear(); - if ($leapyear->isLeapYear($year)) { + $leapYear = new LeapYear(); + if ($leapYear->isLeapYear($year)) { return 'Yep, this is a leap year! '; } @@ -148,12 +147,12 @@ is to convert the controller return value to a proper Response instance, but only if needed:: // example.com/src/Simplex/StringResponseListener.php - namespace Simplex; use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; + use Symfony\Component\HttpKernel\KernelEvents; class StringResponseListener implements EventSubscriberInterface { @@ -168,7 +167,7 @@ only if needed:: public static function getSubscribedEvents() { - return array('kernel.view' => 'onView'); + return array(KernelEvents::VIEW => 'onView'); } } @@ -200,5 +199,3 @@ worlds: a custom framework, tailored to your needs, but based on a rock-solid and well maintained low-level architecture that has been proven to work for many websites; a code that has been audited for security issues and that has proven to scale well. - -.. _`Internals`: http://symfony.com/doc/current/book/internals.html#events diff --git a/create_framework/http-kernel-httpkernelinterface.rst b/create_framework/http_kernel_httpkernelinterface.rst similarity index 85% rename from create_framework/http-kernel-httpkernelinterface.rst rename to create_framework/http_kernel_httpkernelinterface.rst index e0a07731e36..310f4619a1a 100644 --- a/create_framework/http-kernel-httpkernelinterface.rst +++ b/create_framework/http_kernel_httpkernelinterface.rst @@ -8,12 +8,17 @@ goal by making our framework implement ``HttpKernelInterface``:: namespace Symfony\Component\HttpKernel; + // ... interface HttpKernelInterface { /** * @return Response A Response instance */ - function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true); + public function handle( + Request $request, + $type = self::MASTER_REQUEST, + $catch = true + ); } ``HttpKernelInterface`` is probably the most important piece of code in the @@ -26,30 +31,34 @@ Update your framework so that it implements this interface:: // example.com/src/Framework.php // ... - use Symfony\Component\HttpKernel\HttpKernelInterface; class Framework implements HttpKernelInterface { // ... - public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) - { + public function handle( + Request $request, + $type = HttpKernelInterface::MASTER_REQUEST, + $catch = true + ) { // ... } } Even if this change looks trivial, it brings us a lot! Let's talk about one of -the most impressive one: transparent `HTTP caching`_ support. +the most impressive one: transparent :doc:`HTTP caching ` support. The ``HttpCache`` class implements a fully-featured reverse proxy, written in PHP; it implements ``HttpKernelInterface`` and wraps another ``HttpKernelInterface`` instance:: // example.com/web/front.php - $framework = new Simplex\Framework($dispatcher, $matcher, $resolver); - $framework = new HttpKernel\HttpCache\HttpCache($framework, new HttpKernel\HttpCache\Store(__DIR__.'/../cache')); + $framework = new HttpKernel\HttpCache\HttpCache( + $framework, + new HttpKernel\HttpCache\Store(__DIR__.'/../cache') + ); $framework->handle($request)->send(); @@ -61,10 +70,11 @@ to cache a response for 10 seconds, use the ``Response::setTtl()`` method:: // example.com/src/Calendar/Controller/LeapYearController.php + // ... public function indexAction(Request $request, $year) { - $leapyear = new LeapYear(); - if ($leapyear->isLeapYear($year)) { + $leapYear = new LeapYear(); + if ($leapYear->isLeapYear($year)) { $response = new Response('Yep, this is a leap year!'); } else { $response = new Response('Nope, this is not a leap year.'); @@ -130,6 +140,7 @@ early as possible:: if ($response->isNotModified($request)) { return $response; } + $response->setContent('The computed content of the response'); return $response; @@ -138,7 +149,9 @@ Using HTTP caching is great, but what if you cannot cache the whole page? What if you can cache everything but some sidebar that is more dynamic that the rest of the content? Edge Side Includes (`ESI`_) to the rescue! Instead of generating the whole content in one go, ESI allows you to mark a region of a -page as being the content of a sub-request call:: +page as being the content of a sub-request call: + +.. code-block:: text This is the content of your page @@ -153,7 +166,7 @@ sub-requests to convert them to their proper content:: $framework = new HttpKernel\HttpCache\HttpCache( $framework, new HttpKernel\HttpCache\Store(__DIR__.'/../cache'), - new HttpKernel\HttpCache\ESI() + new HttpKernel\HttpCache\Esi() ); .. note:: @@ -166,7 +179,12 @@ When using complex HTTP caching strategies and/or many ESI include tags, it can be hard to understand why and when a resource should be cached or not. To ease debugging, you can enable the debug mode:: - $framework = new HttpCache($framework, new Store(__DIR__.'/../cache'), new ESI(), array('debug' => true)); + $framework = new HttpKernel\HttpCache\HttpCache( + $framework, + new HttpKernel\HttpCache\Store(__DIR__.'/../cache'), + new HttpKernel\HttpCache\Esi(), + array('debug' => true) + ); The debug mode adds a ``X-Symfony-Cache`` header to each response that describes what the cache layer did: @@ -185,6 +203,6 @@ With the addition of a single interface, our framework can now benefit from the many features built into the HttpKernel component; HTTP caching being just one of them but an important one as it can make your applications fly! -.. _`HTTP caching`: http://symfony.com/doc/current/book/http_cache.html -.. _`ESI`: http://en.wikipedia.org/wiki/Edge_Side_Includes +.. _`HTTP caching`: https://symfony.com/doc/current/http_cache.html +.. _`ESI`: https://en.wikipedia.org/wiki/Edge_Side_Includes .. _`Varnish`: https://www.varnish-cache.org/ diff --git a/create_framework/index.rst b/create_framework/index.rst index b6a70f5e264..342a95960ec 100644 --- a/create_framework/index.rst +++ b/create_framework/index.rst @@ -4,14 +4,14 @@ Create your own PHP Framework .. toctree:: introduction - http-foundation - front-controller + http_foundation + front_controller routing templating - http-kernel-controller-resolver - separation-of-concerns - unit-testing - event-dispatcher - http-kernel-httpkernelinterface - http-kernel-httpkernel-class - dependency-injection + http_kernel_controller_resolver + separation_of_concerns + unit_testing + event_dispatcher + http_kernel_httpkernelinterface + http_kernel_httpkernel_class + dependency_injection diff --git a/create_framework/introduction.rst b/create_framework/introduction.rst index 8975d349b37..196d1325f27 100644 --- a/create_framework/introduction.rst +++ b/create_framework/introduction.rst @@ -84,7 +84,7 @@ classes, how you will reference external dependencies, etc. To store your new framework, create a directory somewhere on your machine: -.. code-block:: bash +.. code-block:: terminal $ mkdir framework $ cd framework @@ -94,7 +94,7 @@ Dependency Management To install the Symfony Components that you need for your framework, you are going to use `Composer`_, a project dependency manager for PHP. If you don't have it -yet, :doc:`download and install Composer ` now. +yet, :doc:`download and install Composer ` now. Our Project ----------- @@ -104,23 +104,22 @@ Instead of creating our framework from scratch, we are going to write the same start with the simplest web application we can think of in PHP:: // framework/index.php + $name = $_GET['name']; - $input = $_GET['name']; - - printf('Hello %s', $input); + printf('Hello %s', $name); If you have PHP 5.4, you can use the PHP built-in server to test this great -application in a browser (``http://localhost:4321/index.php?name=Fabien``). -Otherwise, use your own server (Apache, Nginx, etc.): +application in a browser (``http://localhost:4321/index.php?name=Fabien``): -.. code-block:: bash +.. code-block:: terminal $ php -S 127.0.0.1:4321 -In the next chapter, we are going to introduce the HttpFoundation Component -and see what it brings us. +Otherwise, you can always use your own server (Apache, Nginx, etc.). + +In the :doc:`next chapter `, we are going to +introduce the HttpFoundation Component and see what it brings us. -.. _`Symfony`: http://symfony.com/ -.. _`documentation`: http://symfony.com/doc +.. _`Symfony`: https://symfony.com/ .. _`Silex`: http://silex.sensiolabs.org/ .. _`Composer`: http://packagist.org/about-composer diff --git a/create_framework/map.rst.inc b/create_framework/map.rst.inc index 574c0f5e769..0f3bc41cbab 100644 --- a/create_framework/map.rst.inc +++ b/create_framework/map.rst.inc @@ -1,12 +1,12 @@ * :doc:`/create_framework/introduction` -* :doc:`/create_framework/http-foundation` -* :doc:`/create_framework/front-controller` +* :doc:`/create_framework/http_foundation` +* :doc:`/create_framework/front_controller` * :doc:`/create_framework/routing` * :doc:`/create_framework/templating` -* :doc:`/create_framework/http-kernel-controller-resolver` -* :doc:`/create_framework/separation-of-concerns` -* :doc:`/create_framework/unit-testing` -* :doc:`/create_framework/event-dispatcher` -* :doc:`/create_framework/http-kernel-httpkernelinterface` -* :doc:`/create_framework/http-kernel-httpkernel-class` -* :doc:`/create_framework/dependency-injection` +* :doc:`/create_framework/http_kernel_controller_resolver` +* :doc:`/create_framework/separation_of_concerns` +* :doc:`/create_framework/unit_testing` +* :doc:`/create_framework/event_dispatcher` +* :doc:`/create_framework/http_kernel_httpkernelinterface` +* :doc:`/create_framework/http_kernel_httpkernel_class` +* :doc:`/create_framework/dependency_injection` diff --git a/create_framework/routing.rst b/create_framework/routing.rst index c9456b02b04..deaf9a02420 100644 --- a/create_framework/routing.rst +++ b/create_framework/routing.rst @@ -5,7 +5,6 @@ Before we start diving into the Routing component, let's refactor our current framework just a little to make templates even more readable:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -34,8 +33,7 @@ As we now extract the request query parameters, simplify the ``hello.php`` template as follows:: - - Hello + Hello Now, we are in good shape to add new features. @@ -43,17 +41,11 @@ One very important aspect of any website is the form of its URLs. Thanks to the URL map, we have decoupled the URL from the code that generates the associated response, but it is not yet flexible enough. For instance, we might want to support dynamic paths to allow embedding data directly into the URL -instead of relying on a query string: - - # Before - /hello?name=Fabien - - # After - /hello/Fabien +(e.g. ``/hello/Fabien``) instead of relying on a query string (e.g. ``/hello?name=Fabien``). To support this feature, add the Symfony Routing component as a dependency: -.. code-block:: bash +.. code-block:: terminal $ composer require symfony/routing @@ -64,7 +56,7 @@ Instead of an array for the URL map, the Routing component relies on a $routes = new RouteCollection(); -Let's add a route that describe the ``/hello/SOMETHING`` URL and add another +Let's add a route that describes the ``/hello/SOMETHING`` URL and add another one for the simple ``/bye`` one:: use Symfony\Component\Routing\Route; @@ -78,10 +70,12 @@ of default values for route attributes (``array('name' => 'World')``). .. note:: - Read the official `documentation`_ for the Routing component to learn more - about its many features like URL generation, attribute requirements, HTTP - method enforcements, loaders for YAML or XML files, dumpers to PHP or - Apache rewrite rules for enhanced performance, and much more. + Read the + :doc:`Routing component documentation ` to + learn more about its many features like URL generation, attribute + requirements, HTTP method enforcements, loaders for YAML or XML files, + dumpers to PHP or Apache rewrite rules for enhanced performance and much + more. Based on the information stored in the ``RouteCollection`` instance, a ``UrlMatcher`` instance can match URL paths:: @@ -100,21 +94,27 @@ The ``match()`` method takes a request path and returns an array of attributes ``_route`` attribute):: print_r($matcher->match('/bye')); + /* Gives: array ( '_route' => 'bye', ); + */ print_r($matcher->match('/hello/Fabien')); + /* Gives: array ( 'name' => 'Fabien', '_route' => 'hello', ); + */ print_r($matcher->match('/hello')); + /* Gives: array ( 'name' => 'World', '_route' => 'hello', ); + */ .. note:: @@ -130,7 +130,6 @@ The URL matcher throws an exception when none of the routes match:: With this knowledge in mind, let's write the new version of our framework:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -150,9 +149,9 @@ With this knowledge in mind, let's write the new version of our framework:: include sprintf(__DIR__.'/../src/pages/%s.php', $_route); $response = new Response(ob_get_clean()); - } catch (Routing\Exception\ResourceNotFoundException $e) { + } catch (Routing\Exception\ResourceNotFoundException $exception) { $response = new Response('Not Found', 404); - } catch (Exception $e) { + } catch (Exception $exception) { $response = new Response('An error occurred', 500); } @@ -167,15 +166,11 @@ There are a few new things in the code: * Request attributes are extracted to keep our templates simple:: - Hello -* Route configuration has been moved to its own file: - - .. code-block:: php +* Route configuration has been moved to its own file:: // example.com/src/app.php - use Symfony\Component\Routing; $routes = new Routing\RouteCollection(); @@ -206,7 +201,13 @@ impact. Want to know how to use the generator? Insanely easy:: The code should be self-explanatory; and thanks to the context, you can even generate absolute URLs:: - echo $generator->generate('hello', array('name' => 'Fabien'), true); + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + + echo $generator->generate( + 'hello', + array('name' => 'Fabien'), + UrlGeneratorInterface::ABSOLUTE_URL + ); // outputs something like http://example.com/somewhere/hello/Fabien .. tip:: @@ -218,5 +219,3 @@ generate absolute URLs:: $dumper = new Routing\Matcher\Dumper\PhpMatcherDumper($routes); echo $dumper->dump(); - -.. _`documentation`: http://symfony.com/doc/current/components/routing.html diff --git a/create_framework/separation-of-concerns.rst b/create_framework/separation_of_concerns.rst similarity index 86% rename from create_framework/separation-of-concerns.rst rename to create_framework/separation_of_concerns.rst index d7bb049970c..a3c1265697e 100644 --- a/create_framework/separation-of-concerns.rst +++ b/create_framework/separation_of_concerns.rst @@ -8,7 +8,7 @@ class. It would bring us better *reusability* and easier testing to name just a few benefits. If you have a closer look at the code, ``front.php`` has one input, the -Request, and one output, the Response. Our framework class will follow this +Request and one output, the Response. Our framework class will follow this simple principle: the logic is about creating the Response associated with a Request. @@ -16,7 +16,6 @@ Let's create our very own namespace for our framework: ``Simplex``. Move the request handling logic into its own ``Simplex\\Framework`` class:: // example.com/src/Simplex/Framework.php - namespace Simplex; use Symfony\Component\HttpFoundation\Request; @@ -47,9 +46,9 @@ request handling logic into its own ``Simplex\\Framework`` class:: $arguments = $this->resolver->getArguments($request, $controller); return call_user_func_array($controller, $arguments); - } catch (ResourceNotFoundException $e) { + } catch (ResourceNotFoundException $exception) { return new Response('Not Found', 404); - } catch (\Exception $e) { + } catch (\Exception $exception) { return new Response('An error occurred', 500); } } @@ -60,7 +59,6 @@ And update ``example.com/web/front.php`` accordingly:: // example.com/web/front.php // ... - $request = Request::createFromGlobals(); $routes = include __DIR__.'/../src/app.php'; @@ -79,27 +77,22 @@ To wrap up the refactoring, let's move everything but routes definition from For the classes defined under the ``Simplex`` and ``Calendar`` namespaces to be autoloaded, update the ``composer.json`` file: -.. code-block:: javascript +.. code-block:: json { - "require": { - "symfony/http-foundation": "2.5.*", - "symfony/routing": "2.5.*", - "symfony/http-kernel": "2.5.*" - }, + "...": "...", "autoload": { - "psr-0": { "Simplex\\": "src/", "Calendar\\": "src/" } + "psr-4": { "": "src/" } } } .. note:: - For the Composer autoloader to be updated, run ``composer update``. + For the Composer autoloader to be updated, run ``composer dump-autoload``. -Move the controller to ``Calendar\\Controller\\LeapYearController``:: +Move the controller to ``Calendar\Controller\LeapYearController``:: // example.com/src/Calendar/Controller/LeapYearController.php - namespace Calendar\Controller; use Symfony\Component\HttpFoundation\Request; @@ -110,8 +103,8 @@ Move the controller to ``Calendar\\Controller\\LeapYearController``:: { public function indexAction(Request $request, $year) { - $leapyear = new LeapYear(); - if ($leapyear->isLeapYear($year)) { + $leapYear = new LeapYear(); + if ($leapYear->isLeapYear($year)) { return new Response('Yep, this is a leap year!'); } @@ -122,7 +115,6 @@ Move the controller to ``Calendar\\Controller\\LeapYearController``:: And move the ``is_leap_year()`` function to its own class too:: // example.com/src/Calendar/Model/LeapYear.php - namespace Calendar\Model; class LeapYear @@ -141,7 +133,7 @@ Don't forget to update the ``example.com/src/app.php`` file accordingly:: $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array( 'year' => null, - '_controller' => 'Calendar\\Controller\\LeapYearController::indexAction', + '_controller' => 'Calendar\Controller\LeapYearController::indexAction', ))); To sum up, here is the new file layout: @@ -150,7 +142,7 @@ To sum up, here is the new file layout: example.com ├── composer.json - ├── composer.lock + ├── composer.lock ├── src │ ├── app.php │ └── Simplex diff --git a/create_framework/templating.rst b/create_framework/templating.rst index 43e77f92623..a00f5b352fb 100644 --- a/create_framework/templating.rst +++ b/create_framework/templating.rst @@ -17,13 +17,12 @@ Change the template rendering part of the framework to read as follows:: // example.com/web/front.php // ... - try { $request->attributes->add($matcher->match($request->getPathInfo())); $response = call_user_func('render_template', $request); - } catch (Routing\Exception\ResourceNotFoundException $e) { + } catch (Routing\Exception\ResourceNotFoundException $exception) { $response = new Response('Not Found', 404); - } catch (Exception $e) { + } catch (Exception $exception) { $response = new Response('An error occurred', 500); } @@ -50,7 +49,7 @@ rendered:: As ``render_template`` is used as an argument to the PHP ``call_user_func()`` function, we can replace it with any valid PHP `callbacks`_. This allows us to -use a function, an anonymous function, or a method of a class as a +use a function, an anonymous function or a method of a class as a controller... your choice. As a convention, for each route, the associated controller is configured via @@ -64,9 +63,9 @@ the ``_controller`` route attribute:: try { $request->attributes->add($matcher->match($request->getPathInfo())); $response = call_user_func($request->attributes->get('_controller'), $request); - } catch (Routing\Exception\ResourceNotFoundException $e) { + } catch (Routing\Exception\ResourceNotFoundException $exception) { $response = new Response('Not Found', 404); - } catch (Exception $e) { + } catch (Exception $exception) { $response = new Response('An error occurred', 500); } @@ -101,7 +100,6 @@ you can even pass additional arguments to the template:: Here is the updated and improved version of our framework:: // example.com/web/front.php - require_once __DIR__.'/../vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; @@ -127,9 +125,9 @@ Here is the updated and improved version of our framework:: try { $request->attributes->add($matcher->match($request->getPathInfo())); $response = call_user_func($request->attributes->get('_controller'), $request); - } catch (Routing\Exception\ResourceNotFoundException $e) { + } catch (Routing\Exception\ResourceNotFoundException $exception) { $response = new Response('Not Found', 404); - } catch (Exception $e) { + } catch (Exception $exception) { $response = new Response('An error occurred', 500); } @@ -144,7 +142,6 @@ framework does not need to be modified in any way, just create a new ``app.php`` file:: // example.com/src/app.php - use Symfony\Component\Routing; use Symfony\Component\HttpFoundation\Response; @@ -153,7 +150,7 @@ framework does not need to be modified in any way, just create a new $year = date('Y'); } - return 0 == $year % 400 || (0 == $year % 4 && 0 != $year % 100); + return 0 === $year % 400 || (0 === $year % 4 && 0 !== $year % 100); } $routes = new Routing\RouteCollection(); @@ -173,12 +170,12 @@ framework does not need to be modified in any way, just create a new The ``is_leap_year()`` function returns ``true`` when the given year is a leap year, ``false`` otherwise. If the year is ``null``, the current year is tested. The controller is simple: it gets the year from the request -attributes, pass it to the `is_leap_year()`` function, and according to the +attributes, pass it to the ``is_leap_year()`` function, and according to the return value it creates a new Response object. As always, you can decide to stop here and use the framework as is; it's probably all you need to create simple websites like those fancy one-page `websites`_ and hopefully a few others. -.. _`callbacks`: http://php.net/callback#language.types.callback -.. _`websites`: http://kottke.org/08/02/single-serving-sites +.. _`callbacks`: https://php.net/callback#language.types.callback +.. _`websites`: https://kottke.org/08/02/single-serving-sites diff --git a/create_framework/unit-testing.rst b/create_framework/unit_testing.rst similarity index 75% rename from create_framework/unit-testing.rst rename to create_framework/unit_testing.rst index 5dcf1ca89ec..9848cc93f65 100644 --- a/create_framework/unit-testing.rst +++ b/create_framework/unit_testing.rst @@ -14,23 +14,24 @@ using `PHPUnit`_. Create a PHPUnit configuration file in .. code-block:: xml - - ./tests + + + + ./src + + This configuration defines sensible defaults for most PHPUnit settings; more @@ -45,7 +46,6 @@ such interfaces for core objects like the URL matcher and the controller resolver. Modify the framework to make use of them:: // example.com/src/Simplex/Framework.php - namespace Simplex; // ... @@ -70,14 +70,16 @@ resolver. Modify the framework to make use of them:: We are now ready to write our first test:: // example.com/tests/Simplex/Tests/FrameworkTest.php - namespace Simplex\Tests; + use PHPUnit\Framework\TestCase; use Simplex\Framework; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; + use Symfony\Component\Routing; use Symfony\Component\Routing\Exception\ResourceNotFoundException; - class FrameworkTest extends \PHPUnit_Framework_TestCase + class FrameworkTest extends TestCase { public function testNotFoundHandling() { @@ -88,9 +90,12 @@ We are now ready to write our first test:: $this->assertEquals(404, $response->getStatusCode()); } - protected function getFrameworkForException($exception) + private function getFrameworkForException($exception) { - $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); + $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); + // use getMock() on PHPUnit 5.3 or below + // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class); + $matcher ->expects($this->once()) ->method('match') @@ -99,9 +104,9 @@ We are now ready to write our first test:: $matcher ->expects($this->once()) ->method('getContext') - ->will($this->returnValue($this->getMock('Symfony\Component\Routing\RequestContext'))) + ->will($this->returnValue($this->createMock(Routing\RequestContext::class))) ; - $resolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); + $resolver = $this->createMock(ControllerResolverInterface::class); return new Framework($matcher, $resolver); } @@ -114,7 +119,7 @@ are testing that our framework converts this exception to a 404 response. Executing this test is as simple as running ``phpunit`` from the ``example.com`` directory: -.. code-block:: bash +.. code-block:: terminal $ phpunit @@ -142,10 +147,14 @@ Response:: use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerResolver; + // ... public function testControllerResponse() { - $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); + $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class); + // use getMock() on PHPUnit 5.3 or below + // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class); + $matcher ->expects($this->once()) ->method('match') @@ -157,6 +166,11 @@ Response:: } ))) ; + $matcher + ->expects($this->once()) + ->method('getContext') + ->will($this->returnValue($this->createMock(Routing\RequestContext::class))) + ; $resolver = new ControllerResolver(); $framework = new Framework($matcher, $resolver); @@ -174,7 +188,7 @@ the one we have set in the controller. To check that we have covered all possible use cases, run the PHPUnit test coverage feature (you need to enable `XDebug`_ first): -.. code-block:: bash +.. code-block:: terminal $ phpunit --coverage-html=cov/ @@ -182,6 +196,12 @@ Open ``example.com/cov/src/Simplex/Framework.php.html`` in a browser and check that all the lines for the Framework class are green (it means that they have been visited when the tests were executed). +Alternatively you can output the result directly to the console: + +.. code-block:: terminal + + $ phpunit --coverage-text + Thanks to the simple object-oriented code that we have written so far, we have been able to write unit-tests to cover all possible use cases of our framework; test doubles ensured that we were actually testing our code and not @@ -190,6 +210,6 @@ Symfony code. Now that we are confident (again) about the code we have written, we can safely think about the next batch of features we want to add to our framework. -.. _`PHPUnit`: http://phpunit.de/manual/current/en/index.html -.. _`test doubles`: http://phpunit.de/manual/current/en/test-doubles.html -.. _`XDebug`: http://xdebug.org/ +.. _`PHPUnit`: https://phpunit.de/manual/current/en/index.html +.. _`test doubles`: https://phpunit.de/manual/current/en/test-doubles.html +.. _`XDebug`: https://xdebug.org/ diff --git a/debug.rst b/debug.rst new file mode 100644 index 00000000000..49d3074b799 --- /dev/null +++ b/debug.rst @@ -0,0 +1,8 @@ +Debugging +========= + +.. toctree:: + :maxdepth: 1 + :glob: + + debug/* diff --git a/cookbook/debugging.rst b/debug/debugging.rst similarity index 66% rename from cookbook/debugging.rst rename to debug/debugging.rst index 98fae25bad8..56a377e1aeb 100644 --- a/cookbook/debugging.rst +++ b/debug/debugging.rst @@ -14,8 +14,6 @@ configuration is optimized for two main purposes: * Be as similar as possible as the production environment to avoid problems when deploying the project. -.. _cookbook-debugging-disable-bootstrap: - Disabling the Bootstrap File and Class Caching ---------------------------------------------- @@ -61,3 +59,34 @@ locations. To avoid problems, you can either tell your IDE to ignore the PHP cache files, or you can change the extension used by Symfony for these files:: $kernel->loadClassCache('classes', '.php.cache'); + +Useful Debugging Commands +------------------------- + +When developing a large application, it can be hard to keep track of all the +different services, routes and translations. Luckily, Symfony has some commands +that can help you visualize and find the information. + +``debug:container`` + Displays information about the contents of the Symfony container for all public + services. To find only those matching a name, append the name as an argument. + +``debug:config`` + Shows all configured bundles, their class and their alias. + +``debug:event-dispatcher`` + Displays information about all the registered listeners in the event dispatcher. + +``debug:router`` + Displays information about all configured routes in the application as a + table with the name, method, scheme, host and path for each route. + +``debug:translation `` + Shows a table of the translation key, the domain, the translation and the + fallback translation for all known messages, if translations exist for + the given locale. + +.. tip:: + + When in doubt how to use a console command, open the help section by + appending the ``--help`` option. diff --git a/cookbook/deployment/tools.rst b/deployment.rst similarity index 69% rename from cookbook/deployment/tools.rst rename to deployment.rst index c53ed2fa6a5..481efb7c5d0 100644 --- a/cookbook/deployment/tools.rst +++ b/deployment.rst @@ -6,11 +6,10 @@ How to Deploy a Symfony Application =================================== -.. note:: - - Deploying can be a complex and varied task depending on the setup and the - requirements of your application. This article is not a step-by-step guide, - but is a general list of the most common requirements and ideas for deployment. +Deploying a Symfony application can be a complex and varied task depending on +the setup and the requirements of your application. This article is not a step- +by-step guide, but is a general list of the most common requirements and ideas +for deployment. .. _symfony2-deployment-basics: @@ -45,7 +44,7 @@ Basic File Transfer ~~~~~~~~~~~~~~~~~~~ The most basic way of deploying an application is copying the files manually -via ftp/scp (or similar method). This has its disadvantages as you lack control +via FTP/SCP (or similar method). This has its disadvantages as you lack control over the system as the upgrade progresses. This method also requires you to take some manual steps after transferring the files (see `Common Post-Deployment Tasks`_) @@ -53,26 +52,41 @@ Using Source Control ~~~~~~~~~~~~~~~~~~~~ If you're using source control (e.g. Git or SVN), you can simplify by having -your live installation also be a copy of your repository. When you're ready -to upgrade it is as simple as fetching the latest updates from your source -control system. +your live installation also be a copy of your repository. When you're ready to +upgrade it is as simple as fetching the latest updates from your source control +system. When using Git, a common approach is to create a tag for each release +and check out the appropriate tag on deployment (see `Git Tagging`_). This makes updating your files *easier*, but you still need to worry about manually taking other steps (see `Common Post-Deployment Tasks`_). +Using Platforms as a Service +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The specific deployment steps vary greatly from one service provider to another, +so check out the dedicated article for the service of your choose: + +.. toctree:: + :maxdepth: 1 + :glob: + + deployment/* + Using Build Scripts and other Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are also tools to help ease the pain of deployment. Some of them have been specifically tailored to the requirements of Symfony. -`Capistrano`_ with `Symfony plugin`_ - `Capistrano`_ is a remote server automation and deployment tool written in Ruby. - `Symfony plugin`_ is a plugin to ease Symfony related tasks, inspired by `Capifony`_ - (which works only with Capistrano 2 ) +`EasyDeployBundle`_ + A Symfony bundle that adds easy deploy tools to your application. -`sf2debpkg`_ - Helps you build a native Debian package for your Symfony project. +`Deployer`_ + This is another native PHP rewrite of Capistrano, with some ready recipes for + Symfony. + +`Ansistrano`_ + An Ansible role that allows you to configure a powerful deploy via YAML files. `Magallanes`_ This Capistrano-like deployment tool is built in PHP, and may be easier @@ -82,22 +96,18 @@ specifically tailored to the requirements of Symfony. This Python-based library provides a basic suite of operations for executing local or remote shell commands and uploading/downloading files. -Bundles - There are some `bundles that add deployment features`_ directly into your - Symfony console. +`Capistrano`_ with `Symfony plugin`_ + `Capistrano`_ is a remote server automation and deployment tool written in Ruby. + `Symfony plugin`_ is a plugin to ease Symfony related tasks, inspired by `Capifony`_ + (which works only with Capistrano 2). + +`sf2debpkg`_ + Helps you build a native Debian package for your Symfony project. Basic scripting You can of course use shell, `Ant`_ or any other build tool to script the deploying of your project. -Platform as a Service Providers - The Symfony Cookbook includes detailed articles for some of the most well-known - Platform as a Service (PaaS) providers: - - * :doc:`Microsoft Azure ` - * :doc:`Heroku ` - * :doc:`Platform.sh ` - Common Post-Deployment Tasks ---------------------------- @@ -109,15 +119,24 @@ A) Check Requirements Check if your server meets the requirements by running: -.. code-block:: bash +.. code-block:: terminal $ php app/check.php -B) Configure your ``app/config/parameters.yml`` File -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _b-configure-your-app-config-parameters-yml-file: + +B) Configure your Parameters File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This file should *not* be deployed, but managed through the automatic utilities -provided by Symfony. +Most Symfony applications define configuration parameters in a file called +``app/config/parameters.yml``. This file should *not* be deployed, because +Symfony generates it automatically using the ``app/config/parameters.yml.dist`` +file as a template (that's why ``parameters.yml.dist`` must be committed and +deployed). + +If your application uses environment variables instead of these parameters, you +must define those env vars in your production server using the tools provided by +your hosting service. C) Install/Update your Vendors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -127,7 +146,7 @@ update the ``vendor/`` directory, then transfer that with your source code) or afterwards on the server. Either way, just update your vendors as you normally do: -.. code-block:: bash +.. code-block:: terminal $ composer install --no-dev --optimize-autoloader @@ -148,7 +167,7 @@ D) Clear your Symfony Cache Make sure you clear (and warm-up) your Symfony cache: -.. code-block:: bash +.. code-block:: terminal $ php app/console cache:clear --env=prod --no-debug @@ -157,7 +176,7 @@ E) Dump your Assetic Assets If you're using Assetic, you'll also want to dump your assets: -.. code-block:: bash +.. code-block:: terminal $ php app/console assetic:dump --env=prod --no-debug @@ -174,12 +193,12 @@ setup: * Pushing assets to a CDN * ... -Application Lifecycle: Continuous Integration, QA, etc ------------------------------------------------------- +Application Lifecycle: Continuous Integration, QA, etc. +------------------------------------------------------- While this entry covers the technical details of deploying, the full lifecycle -of taking code from development up to production may have a lot more steps -(think deploying to staging, QA (Quality Assurance), running tests, etc). +of taking code from development up to production may have more steps: +deploying to staging, QA (Quality Assurance), running tests, etc. The use of staging, testing, QA, continuous integration, database migrations and the capability to roll back in case of failure are all strongly advised. There @@ -190,14 +209,16 @@ Don't forget that deploying your application also involves updating any dependen (typically via Composer), migrating your database, clearing your cache and other potential things like pushing assets to a CDN (see `Common Post-Deployment Tasks`_). -.. _`Capifony`: http://capifony.org/ +.. _`Git Tagging`: https://git-scm.com/book/en/v2/Git-Basics-Tagging +.. _`Capifony`: https://github.com/everzet/capifony .. _`Capistrano`: http://capistranorb.com/ .. _`sf2debpkg`: https://github.com/liip/sf2debpkg .. _`Fabric`: http://www.fabfile.org/ +.. _`Ansistrano`: https://ansistrano.com/ .. _`Magallanes`: https://github.com/andres-montanez/Magallanes .. _`Ant`: http://blog.sznapka.pl/deploying-symfony2-applications-with-ant -.. _`bundles that add deployment features`: http://knpbundles.com/search?q=deploy .. _`Memcached`: http://memcached.org/ .. _`Redis`: http://redis.io/ .. _`Symfony plugin`: https://github.com/capistrano/symfony/ - +.. _`Deployer`: http://deployer.org/ +.. _`EasyDeployBundle`: https://github.com/EasyCorp/easy-deploy-bundle diff --git a/cookbook/deployment/azure-website.rst b/deployment/azure-website.rst similarity index 73% rename from cookbook/deployment/azure-website.rst rename to deployment/azure-website.rst index e24ed31409e..385ab888363 100644 --- a/cookbook/deployment/azure-website.rst +++ b/deployment/azure-website.rst @@ -4,10 +4,10 @@ Deploying to Microsoft Azure Website Cloud ========================================== -This step by step cookbook describes how to deploy a small Symfony web +This step by step article describes how to deploy a small Symfony web application to the Microsoft Azure Website cloud platform. It will explain how to set up a new Azure website including configuring the right PHP version and -global environment variables. The document also shows how to you can leverage +global environment variables. The document also shows how you can leverage Git and Composer to deploy your Symfony application to the cloud. Setting up the Azure Website @@ -15,10 +15,11 @@ Setting up the Azure Website To set up a new Microsoft Azure Website, first `sign up with Azure`_ or sign in with your credentials. Once you're connected to your `Azure Portal`_ interface, -scroll down to the bottom and select the **New** panel. On this panel, click -**Web Site** and choose **Custom Create**: +select the **New** panel. On this panel, use the search bar, search for +**Web App + MySQL** and choose **Web App + MySQL** by **Microsoft** and +click **Create**: -.. image:: /images/cookbook/deployment/azure-website/step-01.png +.. image:: /_images/deployment/azure-website/step-01.png :alt: Create a new custom Azure Website Step 1: Create Web Site @@ -26,61 +27,54 @@ Step 1: Create Web Site Here, you will be prompted to fill in some basic information. -.. image:: /images/cookbook/deployment/azure-website/step-02.png +.. image:: /_images/deployment/azure-website/step-02.png :alt: Setup the Azure Website -For the URL, enter the URL that you would like to use for your Symfony application, -then pick **Create new web hosting plan** in the region you want. By default, a -*free 20 MB SQL database* is selected in the database dropdown list. In this -tutorial, the Symfony app will connect to a MySQL database. Pick the -**Create a new MySQL database** option in the dropdown list. You can keep -the **DefaultConnection** string name. Finally, check the box -**Publish from source control** to enable a Git repository and go to the -next step. +For the URL, enter the URL that you would like to use for your Symfony +application, then select your **Subscription**, **Create a new Resource Group** +(which is a collection of resources that share the same lifecycle, permissions +and policies). Pick ClearDB as a **Database Provider**. Create a new **App +Service plan/Location** you will be prompted to set up your app service plan +with a name, a region and a pricing tier. Then create a new **Database**, you +will be prompted to set up your MySQL database storage with a database name and +a region. The MySQL database storage is provided by Microsoft in partnership +with ClearDB. Choose the same region you selected for App Service plan. -Step 2: New MySQL Database -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Click Create to continue. -On this step, you will be prompted to set up your MySQL database storage with a -database name and a region. The MySQL database storage is provided by Microsoft -in partnership with ClearDB. Choose the same region you selected for the hosting -plan configuration in the previous step. +Once you created the web site, select **All resources** in the left menu and +choose the website you just created. -.. image:: /images/cookbook/deployment/azure-website/step-03.png - :alt: Setup the MySQL database - -Agree to the terms and conditions and click on the right arrow to continue. - -Step 3: Where Is your Source Code +Step 2: Where Is your Source Code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now, on the third step, select a **Local Git repository** item and click -on the right arrow to configure your Azure Website credentials. +Now, select **Deployment options** under **APP DEPLOYMENT**, select **Choose +Source** and choose **Local Git repository** to configure your Azure Website +credentials. If you choose a different source like GitHub or Bitbucket you can +ignore the next step. -.. image:: /images/cookbook/deployment/azure-website/step-04.png +.. image:: /_images/deployment/azure-website/step-03.png :alt: Setup a local Git repository -Step 4: New Username and Password -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Once you selected **Local Git repository**, click **Setup connection** you will +be prompted to create a username and a secure password: these will become +essential identifiers to connect to the FTP server and also to push your +application code to the Git repository. -Great! You're now on the final step. Create a username and a secure password: -these will become essential identifiers to connect to the FTP server and -also to push your application code to the Git repository. - -.. image:: /images/cookbook/deployment/azure-website/step-05.png +.. image:: /_images/deployment/azure-website/step-04.png :alt: Configure Azure Website credentials Congratulations! Your Azure Website is now up and running. You can check it by browsing to the Website url you configured in the first step. You should see the following display in your web browser: -.. image:: /images/cookbook/deployment/azure-website/step-06.png +.. image:: /_images/deployment/azure-website/step-05.png :alt: Azure Website is running The Microsoft Azure portal also provides a complete control panel for the Azure Website. -.. image:: /images/cookbook/deployment/azure-website/step-07.png +.. image:: /_images/deployment/azure-website/step-06.png :alt: Azure Website Control Panel Your Azure Website is ready! But to run a Symfony site, you need to configure @@ -100,10 +94,10 @@ Even though Symfony only requires PHP 5.3.9 to run, it's always recommended to use the most recent PHP version whenever possible. PHP 5.3 is no longer supported by the PHP core team, but you can update it easily in Azure. -To update your PHP version on Azure, go to the **Configure** tab of the control -panel and select the version you want. +To update your PHP version on Azure, go to the **Application settings** under +**SETTINGS** and select the version you want. -.. image:: /images/cookbook/deployment/azure-website/step-08.png +.. image:: /_images/deployment/azure-website/step-07.png :alt: Enabling the most recent PHP runtime from Azure Website Control Panel Click the **Save** button in the bottom bar to save your changes and restart @@ -117,10 +111,10 @@ the web server. is no need to install and set up APC. The following screenshot shows the output of a :phpfunction:`phpinfo` script - run from an Azure Website to verify that PHP 5.5 is running with + run from an Azure Website to verify that PHP 7.0 is running with OPCache enabled. - .. image:: /images/cookbook/deployment/azure-website/step-09.png + .. image:: /_images/deployment/azure-website/step-08.png :alt: OPCache Configuration Tweaking php.ini Configuration Settings @@ -150,25 +144,24 @@ Website repository. .. note:: - This cookbook has a section dedicated to explaining how to configure your - Azure Website Git repository and how to push the commits to be deployed. See - `Deploying from Git`_. You can also learn more about configuring PHP - internal settings on the official `PHP MSDN documentation`_ page. + `Deploying from Git`_ is dedicated to explaining how to configure your + Azure Website Git repository and how to push the commits to be deployed. + You can also learn more about configuring PHP internal settings on the + official `PHP MSDN documentation`_ page. Enabling the PHP intl Extension ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This is the tricky part of the guide! At the time of writing this cookbook, -Microsoft Azure Website provided the ``intl`` extension, but it's not enabled -by default. To enable the ``intl`` extension, there is no need to upload -any DLL files as the ``php_intl.dll`` file already exists on Azure. In fact, -this file just needs to be moved into the custom website extension directory. +**The** ``intl`` **extension is now enabled by default. The following steps are +no longer necessary.** You can check if the ``intl`` extension is enabled in the +:phpfunction:`phpinfo` page. -.. note:: +However if the ``intl`` extension is not enabled you can follow these steps. - The Microsoft Azure team is currently working on enabling the ``intl`` PHP - extension by default. In the near future, the following steps will no - longer be necessary. +This is the tricky part of the guide! To enable the ``intl`` extension, there is +no need to upload any DLL files as the ``php_intl.dll`` file already exists on +Azure. In fact, this file just needs to be moved into the custom website +extension directory. To get the ``php_intl.dll`` file under your ``site/wwwroot`` directory, simply access the online **Kudu** tool by browsing to the following URL: @@ -182,7 +175,7 @@ explorer, a command line prompt, a log stream and a configuration settings summa page. Of course, this section can only be accessed if you're logged in to your main Azure Website account. -.. image:: /images/cookbook/deployment/azure-website/step-10.png +.. image:: /_images/deployment/azure-website/step-09.png :alt: The Kudu Panel From the Kudu front page, click on the **Debug Console** navigation item in the @@ -193,7 +186,7 @@ In the console prompt, type the following three commands to copy the original ``php_intl.dll`` extension file into a custom website ``ext/`` directory. This new directory must be created under the main directory ``site/wwwroot``. -.. code-block:: bash +.. code-block:: terminal $ cd site\wwwroot $ mkdir ext @@ -201,18 +194,18 @@ new directory must be created under the main directory ``site/wwwroot``. The whole process and output should look like this: -.. image:: /images/cookbook/deployment/azure-website/step-11.png +.. image:: /_images/deployment/azure-website/step-10.png :alt: Executing commands in the online Kudu Console prompt To complete the activation of the ``php_intl.dll`` extension, you must tell Azure Website to load it from the newly created ``ext`` directory. This can be done by registering a global ``PHP_EXTENSIONS`` environment variable from -the **Configure** tab of the main Azure Website Control panel. +the **Application settings** page of the main Azure Website control panel. In the **app settings** section, register the ``PHP_EXTENSIONS`` environment variable with the value ``ext\php_intl.dll`` as shown in the screenshot below: -.. image:: /images/cookbook/deployment/azure-website/step-12.png +.. image:: /_images/deployment/azure-website/step-11.png :alt: Registering custom PHP extensions Hit "save" to confirm your changes and restart the web server. The PHP ``Intl`` @@ -220,7 +213,7 @@ extension should now be available in your web server environment. The following screenshot of a :phpfunction:`phpinfo` page verifies the ``intl`` extension is properly enabled: -.. image:: /images/cookbook/deployment/azure-website/step-13.png +.. image:: /_images/deployment/azure-website/step-12.png :alt: Intl extension is enabled Great! The PHP environment setup is now complete. Next, you'll learn how @@ -233,7 +226,7 @@ Deploying from Git First, make sure Git is correctly installed on your local machine using the following command in your terminal: -.. code-block:: bash +.. code-block:: terminal $ git --version @@ -242,10 +235,10 @@ following command in your terminal: Get your Git from the `git-scm.com`_ website and follow the instructions to install and configure it on your local machine. -In the Azure Website Control panel, browse the **Deployment** tab to get the +In the Azure Website Control panel, browse the **Overview** tab to get the Git repository URL where you should push your code: -.. image:: /images/cookbook/deployment/azure-website/step-14.png +.. image:: /_images/deployment/azure-website/step-13.png :alt: Git deployment panel Now, you'll want to connect your local Symfony application with this remote @@ -281,7 +274,7 @@ Website. Now, from the command line on your local machine, type the following at the root of your Symfony project: -.. code-block:: bash +.. code-block:: terminal $ git remote add azure https://@.scm.azurewebsites.net:443/.git $ git push azure master @@ -296,7 +289,7 @@ Git repository. The deployment with Git should produce an output similar to the screenshot below: -.. image:: /images/cookbook/deployment/azure-website/step-15.png +.. image:: /_images/deployment/azure-website/step-14.png :alt: Deploying files to the Git Azure Website repository The code of the Symfony application has now been deployed to the Azure Website @@ -312,11 +305,11 @@ step is to configure the application and install the third party dependencies it requires that aren't tracked by Git. Switch back to the online **Console** of the Kudu application and execute the following commands in it: -.. code-block:: bash +.. code-block:: terminal $ cd site\wwwroot $ curl -sS https://getcomposer.org/installer | php - $ php -d extension=php_intl.dll composer.phar install + $ php composer.phar install The ``curl`` command retrieves and downloads the Composer command line tool and installs it at the root of the ``site/wwwroot`` directory. Then, running @@ -326,28 +319,20 @@ libraries. This may take a while depending on the number of third-party dependencies you've configured in your ``composer.json`` file. -.. note:: - - The ``-d`` switch allows you to quickly override/add any ``php.ini`` settings. - In this command, we are forcing PHP to use the ``intl`` extension, because - it is not enabled by default in Azure Website at the moment. Soon, this - ``-d`` option will no longer be needed since Microsoft will enable the - ``intl`` extension by default. - At the end of the ``composer install`` command, you will be prompted to fill in the values of some Symfony settings like database credentials, locale, mailer credentials, CSRF token protection, etc. These parameters come from the ``app/config/parameters.yml.dist`` file. -.. image:: /images/cookbook/deployment/azure-website/step-16.png +.. image:: /_images/deployment/azure-website/step-15.png :alt: Configuring Symfony global parameters -The most important thing in this cookbook is to correctly set up your database -settings. You can get your MySQL database settings on the right sidebar of the -**Azure Website Dashboard** panel. Simply click on the -**View Connection Strings** link to make them appear in a pop-in. +The most important thing in this article is to correctly set up your database +settings. You can get your MySQL database settings in the **Application +settings** page. Simply click on the **Show connection string values** link to +make them appear. -.. image:: /images/cookbook/deployment/azure-website/step-17.png +.. image:: /_images/deployment/azure-website/step-16.png :alt: MySQL database settings The displayed MySQL database settings should be something similar to the code @@ -377,27 +362,24 @@ doesn't provide a built-in mailer service. You should consider configuring the host-name and credentials of some other third-party mailing service if your application needs to send emails. -.. image:: /images/cookbook/deployment/azure-website/step-18.png - :alt: Configuring Symfony - Your Symfony application is now configured and should be almost operational. The final step is to build the database schema. This can easily be done with the command line interface if you're using Doctrine. In the online **Console** tool of the Kudu application, run the following command to mount the tables into your MySQL database. -.. code-block:: bash +.. code-block:: terminal $ php app/console doctrine:schema:update --force This command builds the tables and indexes for your MySQL database. If your Symfony application is more complex than a basic Symfony Standard Edition, you -may have additional commands to execute for setup (see :doc:`/cookbook/deployment/tools`). +may have additional commands to execute for setup (see :doc:`/deployment`). Make sure that your application is running by browsing the ``app.php`` front controller with your web browser and the following URL: -.. code-block:: bash +.. code-block:: terminal http://.azurewebsites.net/web/app.php @@ -421,8 +403,6 @@ application, configure it with the following content: .. code-block:: xml - - @@ -436,7 +416,7 @@ application, configure it with the following content: - + @@ -471,8 +451,8 @@ and executed on a Microsoft IIS web server. The process is simple and easy to implement. And as a bonus, Microsoft is continuing to reduce the number of steps needed so that deployment becomes even easier. -.. _`sign up with Azure`: https://signup.live.com/signup.aspx -.. _`Azure Portal`: https://manage.windowsazure.com -.. _`PHP MSDN documentation`: http://blogs.msdn.com/b/silverlining/archive/2012/07/10/configuring-php-in-windows-azure-websites-with-user-ini-files.aspx -.. _`git-scm.com`: http://git-scm.com/download +.. _`sign up with Azure`: https://azure.microsoft.com/free/ +.. _`Azure Portal`: https://portal.azure.com +.. _`PHP MSDN documentation`: https://blogs.msdn.com/b/silverlining/archive/2012/07/10/configuring-php-in-windows-azure-websites-with-user-ini-files.aspx +.. _`git-scm.com`: https://git-scm.com/download .. _`SymfonyAzureEdition`: https://github.com/beberlei/symfony-azure-edition/ diff --git a/deployment/fortrabbit.rst b/deployment/fortrabbit.rst new file mode 100644 index 00000000000..ffe968e813d --- /dev/null +++ b/deployment/fortrabbit.rst @@ -0,0 +1,284 @@ +.. index:: + single: Deployment; Deploying to fortrabbit.com + +Deploying to fortrabbit +======================= + +This step-by-step article describes how to deploy a Symfony web application to +`fortrabbit`_. You can read more about using Symfony with fortrabbit on the +official fortrabbit `Symfony install guide`_. + +Setting up fortrabbit +--------------------- + +Before getting started, you should have done a few things on the fortrabbit side: + +* `Sign up`_; +* Add an SSH key to your Account (to deploy via Git); +* Create an App. + +Preparing your Application +-------------------------- + +You don't need to change any code to deploy a Symfony application to fortrabbit. +But it requires some minor tweaks to its configuration. + +Configure Logging +~~~~~~~~~~~~~~~~~ + +Per default Symfony logs to a file. Modify the ``app/config/config_prod.yml`` file +to redirect it to :phpfunction:`error_log`: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config_prod.yml + monolog: + # ... + handlers: + nested: + type: error_log + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // app/config/config_prod.php + $container->loadFromExtension('monolog', array( + // ... + 'handlers' => array( + 'nested' => array( + 'type' => 'error_log', + ), + ), + )); + +Configuring Database Access & Session Handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use the fortrabbit App Secrets to attain your database credentials. +Create the file ``app/config/config_prod_secrets.php`` with the following +contents:: + + // get the path to the secrects.json file + $secrets = getenv("APP_SECRETS") + if (!$secrets) { + return; + } + + // read the file and decode json to an array + $secrets = json_decode(file_get_contents($secrets), true); + + // set database parameters to the container + if (isset($secrets['MYSQL'])) { + $container->setParameter('database_driver', 'pdo_mysql'); + $container->setParameter('database_host', $secrets['MYSQL']['HOST']); + $container->setParameter('database_name', $secrets['MYSQL']['DATABASE']); + $container->setParameter('database_user', $secrets['MYSQL']['USER']); + $container->setParameter('database_password', $secrets['MYSQL']['PASSWORD']); + } + + // check if the Memcache component is present + if (isset($secrets['MEMCACHE'])) { + $memcache = $secrets['MEMCACHE']; + $handlers = array(); + + foreach (range(1, $memcache['COUNT']) as $num) { + $handlers[] = $memcache['HOST'.$num].':'.$memcache['PORT'.$num]; + } + + // apply ini settings + ini_set('session.save_handler', 'memcached'); + ini_set('session.save_path', implode(',', $handlers)); + + if ("2" === $memcache['COUNT']) { + ini_set('memcached.sess_number_of_replicas', 1); + ini_set('memcached.sess_consistent_hash', 1); + ini_set('memcached.sess_binary', 1); + } + } + +Make sure this file is imported into the main config file: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config_prod.yml + imports: + - { resource: config.yml } + - { resource: config_prod_secrets.php } + + # .. + framework: + session: + # set handler_id to null to use default session handler from php.ini (memcached) + handler_id: ~ + # .. + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config_prod.php + $loader->import('config.php'); + $loader->import('config_prod_secrets.php'); + + $container->loadFromExtension('framework', array( + 'session' => array( + 'handler_id' => null, + ), + )); + + // ... + +Configuring the Environment in the Dashboard +-------------------------------------------- + +PHP Settings +~~~~~~~~~~~~ + +The PHP version and enabled extensions are configurable under the PHP settings +of your App within the fortrabbit Dashboard. + +Environment Variables +~~~~~~~~~~~~~~~~~~~~~ + +Set the ``SYMFONY_ENV`` environment variable to ``prod`` to make sure the right +config files get loaded. ENV vars are configuable in fortrabbit Dashboard as well. + +Document Root +~~~~~~~~~~~~~ + +The document root is configurable for every custom domain you setup for your App. +The default is ``/htdocs``, but for Symfony you probably want to change it to +``/htdocs/web``. You also do so in the fortrabbit Dashboard under ``Domain`` settings. + +Deploying to fortrabbit +----------------------- + +It is assumed that your codebase is under version-control with Git and dependencies +are managed with Composer (locally). + +Every time you push to fortrabbit composer install runs before your code gets +deployed. To finetune the deployment behavior put a `fortrabbit.yml`_. deployment +file (optional) in the project root. + +Add fortrabbit as a (additional) Git remote and add your configuration changes: + +.. code-block:: terminal + + $ git remote add fortrabbit git@deploy.eu2.frbit.com:.git + $ git add composer.json composer.lock + $ git add app/config/config_prod_secrets.php + +Commit and push + +.. code-block:: terminal + + $ git commit -m 'fortrabbit config' + $ git push fortrabbit master -u + +.. note:: + + Replace ```` with the name of your fortrabbit App. + +.. code-block:: terminal + + Commit received, starting build of branch master + + ––––––––––––––––––––––– ∙ƒ ––––––––––––––––––––––– + + B U I L D + + Checksum: + def1bb29911a62de26b1ddac6ef97fc76a5c647b + + Deployment file: + fortrabbit.yml + + Pre-script: + not found + 0ms + + Composer: + - - - + Loading composer repositories with package information + Installing dependencies (including require-dev) from lock file + Nothing to install or update + Generating autoload files + + - - - + 172ms + + Post-script: + not found + 0ms + + R E L E A S E + + Packaging: + 930ms + + Revision: + 1455788127289043421.def1bb29911a62de26b1ddac6ef97fc76a5c647b + + Size: + 9.7MB + + Uploading: + 500ms + + Build & release done in 1625ms, now queued for final distribution. + +.. note:: + + The first ``git push`` takes much longer as all composer dependencies get + downloaded. All subsequent deploys are done within seconds. + +That's it! Your application is being deployed on fortrabbit. More information +about `database migrations and tunneling`_ can be found in the fortrabbit +documentation. + +.. _`fortrabbit`: https://www.fortrabbit.com +.. _`Symfony install guide`: https://help.fortrabbit.com/install-symfony +.. _`fortrabbit.yml`: https://help.fortrabbit.com/deployment-file-v2 +.. _`database migrations and tunneling`: https://help.fortrabbit.com/install-symfony-2#toc-migrate-amp-other-database-commands +.. _`Sign up`: https://dashboard.fortrabbit.com diff --git a/cookbook/deployment/heroku.rst b/deployment/heroku.rst similarity index 93% rename from cookbook/deployment/heroku.rst rename to deployment/heroku.rst index 7ed6216eef9..5438208f4fa 100644 --- a/cookbook/deployment/heroku.rst +++ b/deployment/heroku.rst @@ -4,7 +4,7 @@ Deploying to Heroku Cloud ========================= -This step by step cookbook describes how to deploy a Symfony web application to +This step by step article describes how to deploy a Symfony web application to the Heroku cloud platform. Its contents are based on `the original article`_ published by Heroku. @@ -45,7 +45,7 @@ change the value of ``path`` from # ... nested: # ... - path: "php://stderr" + path: 'php://stderr' Once the application is deployed, run ``heroku logs --tail`` to keep the stream of logs from Heroku open in your terminal. @@ -56,7 +56,7 @@ Creating a new Application on Heroku To create a new Heroku application that you can push to, use the CLI ``create`` command: -.. code-block:: bash +.. code-block:: terminal $ heroku create @@ -119,7 +119,7 @@ directory of the application and add just the following content: If you prefer working on the command console, execute the following commands to create the ``Procfile`` file and to add it to the repository: -.. code-block:: bash +.. code-block:: terminal $ echo "web: bin/heroku-php-apache2 web/" > Procfile $ git add . @@ -147,7 +147,7 @@ environment variable named ``SYMFONY_ENV`` and use that environment if nothing else is explicitly set. As Heroku exposes all `config vars`_ as environment variables, you can issue a single command to prepare your app for a deployment: -.. code-block:: bash +.. code-block:: terminal $ heroku config:set SYMFONY_ENV=prod @@ -167,7 +167,7 @@ variables, you can issue a single command to prepare your app for a deployment: Next up, it's finally time to deploy your application to Heroku. If you are doing this for the very first time, you may see a message such as the following: -.. code-block:: bash +.. code-block:: text The authenticity of host 'heroku.com (50.19.85.132)' can't be established. RSA key fingerprint is 8b:48:5e:67:0e:c9:16:47:32:f2:87:0c:1f:c8:60:ad. @@ -178,7 +178,7 @@ In this case, you need to confirm by typing ``yes`` and hitting ```` key Then, deploy your application executing this command: -.. code-block:: bash +.. code-block:: terminal $ git push heroku master @@ -230,7 +230,7 @@ And that's it! If you now open your browser, either by manually pointing it to the URL ``heroku create`` gave you, or by using the Heroku Toolbelt, the application will respond: -.. code-block:: bash +.. code-block:: terminal $ heroku open Opening mighty-hamlet-1981... done @@ -305,20 +305,20 @@ This is also very useful to build assets on the production system, e.g. with Ass With the next deploy, Heroku compiles your app using the Node.js buildpack and your npm packages become installed. On the other hand, your ``composer.json`` is - now ignored. To compile your app with both buildpacks, Node.js *and* PHP, you can - use a special `multiple buildpack`_. To override buildpack auto-detection, you - need to explicitly set the buildpack URL: - - .. code-block:: bash - - $ heroku buildpacks:set https://github.com/ddollar/heroku-buildpack-multi.git + now ignored. To compile your app with both buildpacks, Node.js *and* PHP, you need + to use both buildpacks. To override buildpack auto-detection, you + need to explicitly set the buildpack: - Next, add a ``.buildpacks`` file to your project, listing the buildpacks you need: - - .. code-block:: text + .. code-block:: terminal - https://github.com/heroku/heroku-buildpack-nodejs.git - https://github.com/heroku/heroku-buildpack-php.git + $ heroku buildpacks:set heroku/nodejs + Buildpack set. Next release on your-application will use heroku/nodejs. + Run git push heroku master to create a new release using this buildpack. + $ heroku buildpacks:set heroku/php --index 2 + Buildpack set. Next release on your-application will use: + 1. heroku/nodejs + 2. heroku/php + Run git push heroku master to create a new release using these buildpacks. With the next deploy, you can benefit from both buildpacks. This setup also enables your Heroku environment to make use of node based automatic build tools like @@ -326,7 +326,7 @@ This is also very useful to build assets on the production system, e.g. with Ass .. _`the original article`: https://devcenter.heroku.com/articles/getting-started-with-symfony2 .. _`sign up with Heroku`: https://signup.heroku.com/signup/dc -.. _`Heroku Toolbelt`: https://devcenter.heroku.com/articles/getting-started-with-php#local-workstation-setup +.. _`Heroku Toolbelt`: https://devcenter.heroku.com/articles/getting-started-with-php#set-up .. _`getting Started with PHP on Heroku`: https://devcenter.heroku.com/articles/getting-started-with-php .. _`ephemeral file system`: https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem .. _`Logplex`: https://devcenter.heroku.com/articles/logplex @@ -336,7 +336,6 @@ This is also very useful to build assets on the production system, e.g. with Ass .. _`custom compile steps`: https://devcenter.heroku.com/articles/php-support#custom-compile-step .. _`custom Composer command`: https://getcomposer.org/doc/articles/scripts.md#writing-custom-commands .. _`Heroku buildpacks`: https://devcenter.heroku.com/articles/buildpacks -.. _`multiple buildpack`: https://github.com/ddollar/heroku-buildpack-multi.git .. _`Grunt`: http://gruntjs.com .. _`gulp`: http://gulpjs.com .. _`Heroku documentation`: https://devcenter.heroku.com/articles/custom-php-settings#nginx diff --git a/cookbook/deployment/platformsh.rst b/deployment/platformsh.rst similarity index 84% rename from cookbook/deployment/platformsh.rst rename to deployment/platformsh.rst index de97cd1111b..212099d87d0 100644 --- a/cookbook/deployment/platformsh.rst +++ b/deployment/platformsh.rst @@ -4,7 +4,7 @@ Deploying to Platform.sh ======================== -This step-by-step cookbook describes how to deploy a Symfony web application to +This step-by-step article describes how to deploy a Symfony web application to `Platform.sh`_. You can read more about using Symfony with Platform.sh on the official `Platform.sh documentation`_. @@ -16,9 +16,8 @@ In this guide, it is assumed your codebase is already versioned with Git. Get a Project on Platform.sh ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You need to subscribe to a `Platform.sh project`_. Choose the development plan -and go through the checkout process. Once your project is ready, give it a name -and choose: **Import an existing site**. +You need to `subscribe to a Platform.sh plan`_ and go through the checkout process. +Once your project is ready, give it a name and choose: **Import an existing site**. Prepare Your Application ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -38,30 +37,32 @@ Platform.sh how to deploy your application (read more about # The name of this app. Must be unique within a project. name: myphpproject - # The toolstack used to build the application. - toolstack: "php:symfony" + # The type of the application to build. + type: php:5.6 + build: + flavor: composer # The relationships of the application with services or other applications. # The left-hand side is the name of the relationship as it will be exposed # to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand # side is in the form `:`. relationships: - database: "mysql:mysql" + database: 'mysql:mysql' # The configuration of app when it is exposed to the web. web: # The public directory of the app, relative to its root. - document_root: "/web" + document_root: '/web' # The front-controller script to send non-static requests to. - passthru: "/app.php" + passthru: '/app.php' # The size of the persistent disk of the application (in MB). disk: 2048 # The mounts that will be performed when the package is deployed. mounts: - "/app/cache": "shared:files/cache" - "/app/logs": "shared:files/logs" + '/app/cache': 'shared:files/cache' + '/app/logs': 'shared:files/logs' # The hooks that will be performed when the package is deployed. hooks: @@ -80,7 +81,7 @@ your Git repository which contains the following files: "http://{default}/": type: upstream # the first part should be your project name - upstream: "myphpproject:php" + upstream: 'myphpproject:php' .. code-block:: yaml @@ -124,12 +125,14 @@ following file (it's your role to add this file to your code base):: # Store session into /tmp. ini_set('session.save_path', '/tmp/sessions'); -Make sure this file is listed in your *imports*: +Make sure this file is listed in your *imports* (after the default ``parameters.yml`` +file): .. code-block:: yaml # app/config/config.yml imports: + - { resource: parameters.yml } - { resource: parameters_platform.php } Deploy your Application @@ -138,7 +141,7 @@ Deploy your Application Now you need to add a remote to Platform.sh in your Git repository (copy the command that you see on the Platform.sh web UI): -.. code-block:: bash +.. code-block:: terminal $ git remote add platform [PROJECT-ID]@git.[CLUSTER].platform.sh:[PROJECT-ID].git @@ -149,7 +152,7 @@ command that you see on the Platform.sh web UI): Commit the Platform.sh specific files created in the previous section: -.. code-block:: bash +.. code-block:: terminal $ git add .platform.app.yaml .platform/* $ git add app/config/config.yml app/config/parameters_platform.php @@ -157,7 +160,7 @@ Commit the Platform.sh specific files created in the previous section: Push your code base to the newly added remote: -.. code-block:: bash +.. code-block:: terminal $ git push platform master @@ -183,9 +186,10 @@ That's it! Your Symfony application will be bootstrapped and deployed. You'll soon be able to see it in your browser. .. _`Platform.sh`: https://platform.sh -.. _`Platform.sh documentation`: https://docs.platform.sh/toolstacks/symfony/symfony-getting-started -.. _`Platform.sh project`: https://marketplace.commerceguys.com/platform/buy-now -.. _`Platform.sh configuration files`: https://docs.platform.sh/reference/configuration-files +.. _`Platform.sh documentation`: https://docs.platform.sh/frameworks/symfony.html +.. _`Platform.sh project`: https://accounts.platform.sh/platform/buy-now +.. _`subscribe to a Platform.sh plan`: https://accounts.platform.sh/platform/buy-now +.. _`Platform.sh configuration files`: https://docs.platform.sh/configuration/services.html .. _`GitHub`: https://github.com/platformsh/platformsh-examples .. _`available services`: https://docs.platform.sh/reference/configuration-files/#configure-services -.. _`migrating your database and files`: https://docs.platform.sh/toolstacks/php/symfony/migrate-existing-site/ +.. _`migrating your database and files`: https://docs.platform.sh/tutorials/migrating.html diff --git a/cookbook/request/load_balancer_reverse_proxy.rst b/deployment/proxies.rst similarity index 68% rename from cookbook/request/load_balancer_reverse_proxy.rst rename to deployment/proxies.rst index a57bf88f0f2..e0c6de09667 100644 --- a/cookbook/request/load_balancer_reverse_proxy.rst +++ b/deployment/proxies.rst @@ -2,8 +2,8 @@ How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy ========================================================================== When you deploy your application, you may be behind a load balancer (e.g. -an AWS Elastic Load Balancer) or a reverse proxy (e.g. Varnish for -:doc:`caching`). +an AWS Elastic Load Balancing) or a reverse proxy (e.g. Varnish for +:doc:`caching`). For the most part, this doesn't cause any problems with Symfony. But, when a request passes through a proxy, certain request information is sent using @@ -23,7 +23,7 @@ via HTTPS, the client's port and the hostname being requested. Solution: trusted_proxies ------------------------- -This is no problem, but you *do* need to tell Symfony that this is happening +This is no problem, but you *do* need to tell Symfony what is happening and which reverse proxy IP addresses will be doing this type of thing: .. configuration-block:: @@ -42,12 +42,13 @@ and which reverse proxy IP addresses will be doing this type of thing: + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + .. code-block:: php @@ -62,6 +63,10 @@ the IP address ``192.0.0.1`` or matches the range of IP addresses that use the CIDR notation ``10.0.0.0/8``. For more details, see the :ref:`framework.trusted_proxies ` option. +You are also saying that you trust that the proxy does not send conflicting +headers, e.g. sending both ``X-Forwarded-For`` and ``Forwarded`` in the same +request. + That's it! Symfony will now look for the correct headers to get information like the client's IP address, host, port and whether the request is using HTTPS. @@ -69,7 +74,7 @@ using HTTPS. But what if the IP of my Reverse Proxy Changes Constantly! ---------------------------------------------------------- -Some reverse proxies (like Amazon's Elastic Load Balancers) don't have a +Some reverse proxies (like AWS Elastic Load Balancing) don't have a static IP address or even a range that you can target with the CIDR notation. In this case, you'll need to - *very carefully* - trust *all* proxies. @@ -78,23 +83,48 @@ In this case, you'll need to - *very carefully* - trust *all* proxies. #. Once you've guaranteed that traffic will only come from your trusted reverse proxies, configure Symfony to *always* trust incoming request. This is - done inside of your front controller:: + done inside of your front controller: - // web/app.php + .. code-block:: diff - // ... - Request::setTrustedProxies(array($request->server->get('REMOTE_ADDR'))); + // web/app.php - $response = $kernel->handle($request); - // ... + // ... + $request = Request::createFromGlobals(); + + Request::setTrustedProxies(array('127.0.0.1', $request->server->get('REMOTE_ADDR'))); -#. Ensure that the trusted_proxies setting in your ``app/config/config.yml`` - is not set or it will overwrite the ``setTrustedProxies`` call above. + // ... + +#. Ensure that the trusted_proxies setting in your ``app/config/config.yml`` + is not set or it will overwrite the ``setTrustedProxies()`` call above. That's it! It's critical that you prevent traffic from all non-trusted sources. If you allow outside traffic, they could "spoof" their true IP address and other information. +.. _request-untrust-header: + +My Reverse Proxy Sends X-Forwarded-For but Does not Filter the Forwarded Header +------------------------------------------------------------------------------- + +Many popular proxy implementations do not yet support the ``Forwarded`` header +and do not filter it by default. Ideally, you would configure this in your +proxy. If this is not possible, you can tell Symfony to distrust the ``Forwarded`` +header, while still trusting your proxy's ``X-Forwarded-For`` header. + +This is done inside of your front controller:: + + // web/app.php + + // ... + Request::setTrustedHeaderName(Request::HEADER_FORWARDED, null); + + $response = $kernel->handle($request); + // ... + +Configuring the proxy server trust is very important, as not doing so will +allow malicious users to "spoof" their IP address. + My Reverse Proxy Uses Non-Standard (not X-Forwarded) Headers ------------------------------------------------------------ @@ -107,5 +137,5 @@ these (see ":doc:`/components/http_foundation/trusting_proxies`"). The code for doing this will need to live in your front controller (e.g. ``web/app.php``). -.. _`security groups`: http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/using-elb-security-groups.html +.. _`security groups`: http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html .. _`RFC 7239`: http://tools.ietf.org/html/rfc7239 diff --git a/doctrine.rst b/doctrine.rst new file mode 100644 index 00000000000..00de1cf1b58 --- /dev/null +++ b/doctrine.rst @@ -0,0 +1,840 @@ +.. index:: + single: Doctrine + +Databases and the Doctrine ORM +============================== + +One of the most common and challenging tasks for any application +involves persisting and reading information to and from a database. Although +the Symfony Framework doesn't integrate any component to work with databases, +it provides tight integration with a third-party library called `Doctrine`_. +Doctrine's sole goal is to give you powerful tools to make database interactions +easy and flexible. + +In this chapter, you'll learn how to start leveraging Doctrine in your Symfony projects +to give you rich database interactions. + +.. note:: + + Doctrine is totally decoupled from Symfony and using it is optional. + This chapter is all about the Doctrine ORM, which aims to let you map + objects to a relational database (such as *MySQL*, *PostgreSQL* or + *Microsoft SQL*). If you prefer to use raw database queries, this is + easy, and explained in the ":doc:`/doctrine/dbal`" article. + + You can also persist data to `MongoDB`_ using Doctrine ODM library. For + more information, read the "`DoctrineMongoDBBundle`_" + documentation. + +A Simple Example: A Product +--------------------------- + +The easiest way to understand how Doctrine works is to see it in action. +In this section, you'll configure your database, create a ``Product`` object, +persist it to the database and fetch it back out. + +Configuring the Database +~~~~~~~~~~~~~~~~~~~~~~~~ + +Before you really begin, you'll need to configure your database connection +information. By convention, this information is usually configured in an +``app/config/parameters.yml`` file: + +.. code-block:: yaml + + # app/config/parameters.yml + parameters: + database_host: localhost + database_name: test_project + database_user: root + database_password: password + + # ... + +.. note:: + + Defining the configuration via ``parameters.yml`` is just a convention. + The parameters defined in that file are referenced by the main configuration + file when setting up Doctrine: + + .. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine: + dbal: + driver: pdo_mysql + host: '%database_host%' + dbname: '%database_name%' + user: '%database_user%' + password: '%database_password%' + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine', array( + 'dbal' => array( + 'driver' => 'pdo_mysql', + 'host' => '%database_host%', + 'dbname' => '%database_name%', + 'user' => '%database_user%', + 'password' => '%database_password%', + ), + )); + + By separating the database information into a separate file, you can + easily keep different versions of the file on each server. You can also + easily store database configuration (or any sensitive information) outside + of your project, like inside your Apache configuration, for example. For + more information, see :doc:`/configuration/external_parameters`. + +Now that Doctrine can connect to your database, the following command +can automatically generate an empty ``test_project`` database for you: + +.. code-block:: terminal + + $ php app/console doctrine:database:create + +.. sidebar:: Setting up the Database to be UTF8 + + One mistake even seasoned developers make when starting a Symfony project + is forgetting to set up default charset and collation on their database, + ending up with latin type collations, which are default for most databases. + They might even remember to do it the very first time, but forget that + it's all gone after running a relatively common command during development: + + .. code-block:: terminal + + $ php app/console doctrine:database:drop --force + $ php app/console doctrine:database:create + + There's no way to configure these defaults inside Doctrine, as it tries to be + as agnostic as possible in terms of environment configuration. One way to solve + this problem is to configure server-level defaults. + + Setting UTF8 defaults for MySQL is as simple as adding a few lines to + your configuration file (typically ``my.cnf``): + + .. code-block:: ini + + [mysqld] + # Version 5.5.3 introduced "utf8mb4", which is recommended + collation-server = utf8mb4_unicode_ci # Replaces utf8_unicode_ci + character-set-server = utf8mb4 # Replaces utf8 + + We recommend against MySQL's ``utf8`` character set, since it does not + support 4-byte unicode characters, and strings containing them will be + truncated. This is fixed by the `newer utf8mb4 character set`_. + +.. note:: + + If you want to use SQLite as your database, you need to set the path + where your database file should be stored: + + .. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine: + dbal: + driver: pdo_sqlite + path: '%kernel.root_dir%/sqlite.db' + charset: UTF8 + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine', array( + 'dbal' => array( + 'driver' => 'pdo_sqlite', + 'path' => '%kernel.root_dir%/sqlite.db', + 'charset' => 'UTF-8', + ), + )); + +Creating an Entity Class +~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you're building an application where products need to be displayed. +Without even thinking about Doctrine or databases, you already know that +you need a ``Product`` object to represent those products. Create this class +inside the ``Entity`` directory of your AppBundle:: + + // src/AppBundle/Entity/Product.php + namespace AppBundle\Entity; + + class Product + { + private $name; + private $price; + private $description; + } + +The class - often called an "entity", meaning *a basic class that holds data* - +is simple and helps fulfill the business requirement of needing products +in your application. This class can't be persisted to a database yet - it's +just a simple PHP class. + +.. tip:: + + Once you learn the concepts behind Doctrine, you can have Doctrine create + simple entity classes for you. This will ask you interactive questions + to help you build any entity: + + .. code-block:: terminal + + $ php app/console doctrine:generate:entity + +.. index:: + single: Doctrine; Adding mapping metadata + +.. _doctrine-adding-mapping: + +Add Mapping Information +~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine allows you to work with databases in a much more interesting way +than just fetching rows of scalar data into an array. Instead, Doctrine +allows you to fetch entire *objects* out of the database, and to persist +entire objects to the database. For Doctrine to be able to do this, you +must *map* your database tables to specific PHP classes, and the columns +on those tables must be mapped to specific properties on their corresponding +PHP classes. + +.. image:: /_images/doctrine/mapping_single_entity.png + :align: center + +You'll provide this mapping information in the form of "metadata", a collection +of rules that tells Doctrine exactly how the ``Product`` class and its +properties should be *mapped* to a specific database table. This metadata +can be specified in a number of different formats, including YAML, XML or +directly inside the ``Product`` class via DocBlock annotations: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/AppBundle/Entity/Product.php + namespace AppBundle\Entity; + + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity + * @ORM\Table(name="product") + */ + class Product + { + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @ORM\Column(type="string", length=100) + */ + private $name; + + /** + * @ORM\Column(type="decimal", scale=2) + */ + private $price; + + /** + * @ORM\Column(type="text") + */ + private $description; + } + + .. code-block:: yaml + + # src/AppBundle/Resources/config/doctrine/Product.orm.yml + AppBundle\Entity\Product: + type: entity + table: product + id: + id: + type: integer + generator: { strategy: AUTO } + fields: + name: + type: string + length: 100 + price: + type: decimal + scale: 2 + description: + type: text + + .. code-block:: xml + + + + + + + + + + + + + + + +.. note:: + + A bundle can accept only one metadata definition format. For example, it's + not possible to mix YAML metadata definitions with annotated PHP entity + class definitions. + +.. tip:: + + The table name is optional and if omitted, will be determined automatically + based on the name of the entity class. + +Doctrine allows you to choose from a wide variety of different field types, +each with their own options. For information on the available field types, +see the :ref:`doctrine-field-types` section. + +.. seealso:: + + You can also check out Doctrine's `Basic Mapping Documentation`_ for + all details about mapping information. If you use annotations, you'll + need to prepend all annotations with ``ORM\`` (e.g. ``ORM\Column(...)``), + which is not shown in Doctrine's documentation. You'll also need to include + the ``use Doctrine\ORM\Mapping as ORM;`` statement, which *imports* the + ``ORM`` annotations prefix. + +.. caution:: + + Be careful if the names of your entity classes (or their properties) + are also reserved SQL keywords like ``GROUP`` or ``USER``. For example, + if your entity's class name is ``Group``, then, by default, the corresponding + table name would be ``group``. This will cause an SQL error in some database + engines. See Doctrine's `Reserved SQL keywords documentation`_ for details + on how to properly escape these names. Alternatively, if you're free + to choose your database schema, simply map to a different table name + or column name. See Doctrine's `Creating Classes for the Database`_ + and `Property Mapping`_ documentation. + +.. note:: + + When using another library or program (e.g. Doxygen) that uses annotations, + you should place the ``@IgnoreAnnotation`` annotation on the class to + indicate which annotations Symfony should ignore. + + For example, to prevent the ``@fn`` annotation from throwing an exception, + add the following:: + + /** + * @IgnoreAnnotation("fn") + */ + class Product + // ... + +.. tip:: + + After creating your entities you should validate the mappings with the + following command: + + .. code-block:: terminal + + $ php app/console doctrine:schema:validate + +.. _doctrine-generating-getters-and-setters: + +Generating Getters and Setters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Even though Doctrine now knows how to persist a ``Product`` object to the +database, the class itself isn't really useful yet. Since ``Product`` is just +a regular PHP class with ``private`` properties, you need to create ``public`` +getter and setter methods (e.g. ``getName()``, ``setName($name)``) in order +to access its properties in the rest of your application's code. Add these +methods manually or with your own IDE. + +.. _doctrine-creating-the-database-tables-schema: + +Creating the Database Tables/Schema +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You now have a usable ``Product`` class with mapping information so that +Doctrine knows exactly how to persist it. Of course, you don't yet have the +corresponding ``product`` table in your database. Fortunately, Doctrine can +automatically create all the database tables needed for every known entity +in your application. To do this, run: + +.. code-block:: terminal + + $ php app/console doctrine:schema:update --force + +.. tip:: + + Actually, this command is incredibly powerful. It compares what + your database *should* look like (based on the mapping information of + your entities) with how it *actually* looks, and executes the SQL statements + needed to *update* the database schema to where it should be. In other + words, if you add a new property with mapping metadata to ``Product`` + and run this command, it will execute the "ALTER TABLE" statement needed + to add that new column to the existing ``product`` table. + + An even better way to take advantage of this functionality is via + `migrations`_, which allow you to generate these SQL statements and store + them in migration classes that can be run systematically on your production + server in order to update and track changes to your database schema safely + and reliably. + + Whether or not you take advantage of migrations, the ``doctrine:schema:update`` + command should only be used during development. It should not be used in + a production environment. + +Your database now has a fully-functional ``product`` table with columns that +match the metadata you've specified. + +Persisting Objects to the Database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that you have mapped the ``Product`` entity to its corresponding ``product`` +table, you're ready to persist ``Product`` objects to the database. From inside +a controller, this is pretty easy. Add the following method to the +``DefaultController`` of the bundle:: + + // src/AppBundle/Controller/DefaultController.php + + // ... + use AppBundle\Entity\Product; + use Symfony\Component\HttpFoundation\Response; + + // ... + public function createAction() + { + $product = new Product(); + $product->setName('Keyboard'); + $product->setPrice(19.99); + $product->setDescription('Ergonomic and stylish!'); + + $entityManager = $this->getDoctrine()->getManager(); + + // tells Doctrine you want to (eventually) save the Product (no queries yet) + $entityManager->persist($product); + + // actually executes the queries (i.e. the INSERT query) + $entityManager->flush(); + + return new Response('Saved new product with id '.$product->getId()); + } + +.. note:: + + If you're following along with this example, you'll need to create a + route that points to this action to see it work. + +.. tip:: + + This article shows working with Doctrine from within a controller by using + the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getDoctrine` + method of the controller. This method is a shortcut to get the + ``doctrine`` service. You can work with Doctrine anywhere else + by injecting that service in the service. See + :doc:`/service_container` for more on creating your own services. + +Take a look at the previous example in more detail: + +* **lines 10-13** In this section, you instantiate and work with the ``$product`` + object like any other normal PHP object. + +* **line 15** This line fetches Doctrine's *entity manager* object, which is + responsible for the process of persisting objects to, and fetching objects + from, the database. + +* **line 18** The ``persist($product)`` call tells Doctrine to "manage" the + ``$product`` object. This does **not** cause a query to be made to the database. + +* **line 21** When the ``flush()`` method is called, Doctrine looks through + all of the objects that it's managing to see if they need to be persisted + to the database. In this example, the ``$product`` object's data doesn't + exist in the database, so the entity manager executes an ``INSERT`` query, + creating a new row in the ``product`` table. + +.. note:: + + In fact, since Doctrine is aware of all your managed entities, when you call + the ``flush()`` method, it calculates an overall changeset and executes + the queries in the correct order. It utilizes cached prepared statement to + slightly improve the performance. For example, if you persist a total of 100 + ``Product`` objects and then subsequently call ``flush()``, Doctrine will + execute 100 ``INSERT`` queries using a single prepared statement object. + +.. note:: + + If the ``flush()`` call fails, a ``Doctrine\ORM\ORMException`` exception + is thrown. See `Transactions and Concurrency`_. + +Whether creating or updating objects, the workflow is always the same. In +the next section, you'll see how Doctrine is smart enough to automatically +issue an ``UPDATE`` query if the entity already exists in the database. + +.. tip:: + + Doctrine provides a library that allows you to programmatically load testing + data into your project (i.e. "fixture data"). For information, see + the "`DoctrineFixturesBundle`_" documentation. + +Fetching Objects from the Database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fetching an object back out of the database is even easier. For example, +suppose you've configured a route to display a specific ``Product`` based +on its ``id`` value:: + + public function showAction($productId) + { + $product = $this->getDoctrine() + ->getRepository(Product::class) + ->find($productId); + + if (!$product) { + throw $this->createNotFoundException( + 'No product found for id '.$productId + ); + } + + // ... do something, like pass the $product object into a template + } + +.. tip:: + + You can achieve the equivalent of this without writing any code by using + the ``@ParamConverter`` shortcut. See the `FrameworkExtraBundle documentation`_ + for more details. + +When you query for a particular type of object, you always use what's known +as its "repository". You can think of a repository as a PHP class whose only +job is to help you fetch entities of a certain class. You can access the +repository object for an entity class via:: + + $repository = $this->getDoctrine() + ->getRepository(Product::class); + +.. note:: + + You can also use ``AppBundle:Product`` syntax. This string is a shortcut you can use anywhere + in Doctrine instead of the full class name of the entity (i.e. ``AppBundle\Entity\Product``). + As long as your entity lives under the ``Entity`` namespace of your bundle, + this will work. + +Once you have a repository object, you can access all sorts of helpful methods:: + + $repository = $this->getDoctrine()->getRepository(Product::class); + + // looks for a single product by its primary key (usually "id") + $product = $repository->find($productId); + + // dynamic method names to find a single product based on a column value + $product = $repository->findOneById($productId); + $product = $repository->findOneByName('Keyboard'); + + // dynamic method names to find a group of products based on a column value + $products = $repository->findByPrice(19.99); + + // finds *all* products + $products = $repository->findAll(); + +.. note:: + + Of course, you can also issue complex queries, which you'll learn more + about in the :ref:`doctrine-queries` section. + +You can also take advantage of the useful ``findBy()`` and ``findOneBy()`` methods +to easily fetch objects based on multiple conditions:: + + $repository = $this->getDoctrine()->getRepository(Product::class); + + // looks for a single product matching the given name and price + $product = $repository->findOneBy( + array('name' => 'Keyboard', 'price' => 19.99) + ); + + // looks for multiple products matching the given name, ordered by price + $products = $repository->findBy( + array('name' => 'Keyboard'), + array('price' => 'ASC') + ); + +.. tip:: + + When rendering a page requires to make some database calls, the web debug + toolbar at the bottom of the page displays the number of queries and the + time it took to execute them: + + .. image:: /_images/doctrine/doctrine_web_debug_toolbar.png + :align: center + :class: with-browser + + If the number of database queries is too high, the icon will turn yellow to + indicate that something may not be correct. Click on the icon to open the + Symfony Profiler and see the exact queries that were executed. + +Updating an Object +~~~~~~~~~~~~~~~~~~ + +Once you've fetched an object from Doctrine, updating it is easy. Suppose +you have a route that maps a product id to an update action in a controller:: + + use AppBundle\Entity\Product; + // ... + + public function updateAction($productId) + { + $entityManager = $this->getDoctrine()->getManager(); + $product = $entityManager->getRepository(Product::class)->find($productId); + + if (!$product) { + throw $this->createNotFoundException( + 'No product found for id '.$productId + ); + } + + $product->setName('New product name!'); + $entityManager->flush(); + + return $this->redirectToRoute('homepage'); + } + +Updating an object involves just three steps: + +#. fetching the object from Doctrine; +#. modifying the object; +#. calling ``flush()`` on the entity manager. + +Notice that calling ``$entityManager->persist($product)`` isn't necessary. Recall that +this method simply tells Doctrine to manage or "watch" the ``$product`` object. +In this case, since you fetched the ``$product`` object from Doctrine, it's +already managed. + +Deleting an Object +~~~~~~~~~~~~~~~~~~ + +Deleting an object is very similar, but requires a call to the ``remove()`` +method of the entity manager:: + + $entityManager->remove($product); + $entityManager->flush(); + +As you might expect, the ``remove()`` method notifies Doctrine that you'd +like to remove the given object from the database. The actual ``DELETE`` query, +however, isn't actually executed until the ``flush()`` method is called. + +.. _doctrine-queries: + +Querying for Objects +-------------------- + +You've already seen how the repository object allows you to run basic queries +without any work:: + + $repository = $this->getDoctrine()->getRepository(Product::class); + + $product = $repository->find($productId); + $product = $repository->findOneByName('Keyboard'); + +Of course, Doctrine also allows you to write more complex queries using the +Doctrine Query Language (DQL). DQL is similar to SQL except that you should +imagine that you're querying for one or more objects of an entity class (e.g. ``Product``) +instead of querying for rows on a table (e.g. ``product``). + +When querying in Doctrine, you have two main options: writing pure DQL queries +or using Doctrine's Query Builder. + +Querying for Objects with DQL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Imagine that you want to query for products that cost more than ``19.99``, +ordered from least to most expensive. You can use DQL, Doctrine's native +SQL-like language, to construct a query for this scenario:: + + $entityManager = $this->getDoctrine()->getManager(); + $query = $entityManager->createQuery( + 'SELECT p + FROM AppBundle:Product p + WHERE p.price > :price + ORDER BY p.price ASC' + )->setParameter('price', 19.99); + + $products = $query->getResult(); + +If you're comfortable with SQL, then DQL should feel very natural. The biggest +difference is that you need to think in terms of selecting PHP objects, +instead of rows in a database. For this reason, you select *from* the +``AppBundle:Product`` *entity* (an optional shortcut for the +``AppBundle\Entity\Product`` class) and then alias it as ``p``. + +.. tip:: + + Take note of the ``setParameter()`` method. When working with Doctrine, + it's always a good idea to set any external values as "placeholders" + (``:price`` in the example above) as it prevents SQL injection attacks. + +The ``getResult()`` method returns an array of results. To get only one +result, you can use ``getOneOrNullResult()``:: + + $product = $query->setMaxResults(1)->getOneOrNullResult(); + +The DQL syntax is incredibly powerful, allowing you to easily join between +entities (the topic of :doc:`relations ` will be +covered later), group, etc. For more information, see the official +`Doctrine Query Language`_ documentation. + +Querying for Objects Using Doctrine's Query Builder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of writing a DQL string, you can use a helpful object called the +``QueryBuilder`` to build that string for you. This is useful when the actual query +depends on dynamic conditions, as your code soon becomes hard to read with +DQL as you start to concatenate strings:: + + $repository = $this->getDoctrine() + ->getRepository(Product::class); + + // createQueryBuilder() automatically selects FROM AppBundle:Product + // and aliases it to "p" + $query = $repository->createQueryBuilder('p') + ->where('p.price > :price') + ->setParameter('price', '19.99') + ->orderBy('p.price', 'ASC') + ->getQuery(); + + $products = $query->getResult(); + // to get just one result: + // $product = $query->setMaxResults(1)->getOneOrNullResult(); + +The ``QueryBuilder`` object contains every method necessary to build your +query. By calling the ``getQuery()`` method, the query builder returns a +normal ``Query`` object, which can be used to get the result of the query. + +For more information on Doctrine's Query Builder, consult Doctrine's +`Query Builder`_ documentation. + +Organizing Custom Queries into Repository Classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All the queries in the previous sections were written directly in your controller. +But for organization, Doctrine provides special repository classes that allow you +to keep all your query logic in one, central place. + +see :doc:`/doctrine/repository` for info. + +Configuration +------------- + +Doctrine is highly configurable, though you probably won't ever need to worry +about most of its options. To find out more about configuring Doctrine, see +the Doctrine section of the :doc:`config reference `. + +.. _doctrine-field-types: + +Doctrine Field Types Reference +------------------------------ + +Doctrine comes with numerous field types available. Each of these +maps a PHP data type to a specific column type in whatever database you're +using. For each field type, the ``Column`` can be configured further, setting +the ``length``, ``nullable`` behavior, ``name`` and other options. To see a +list of all available types and more information, see Doctrine's +`Mapping Types documentation`_. + +Relationships and Associations +------------------------------ + +Doctrine provides all the functionality you need to manage database relationships +(also known as associations). For info, see :doc:`/doctrine/associations`. + +Final Thoughts +-------------- + +With Doctrine, you can focus on your *objects* and how they're used in your +application and worry about database persistence second. This is because +Doctrine allows you to use any PHP object to hold your data and relies on +mapping metadata information to map an object's data to a particular database +table. + +Doctrine has a lot more complex features to learn, like relationships, complex queries, +and event listeners. + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + doctrine/* + +* `DoctrineFixturesBundle`_ +* `DoctrineMongoDBBundle`_ + +.. _`Doctrine`: http://www.doctrine-project.org/ +.. _`MongoDB`: https://www.mongodb.org/ +.. _`Basic Mapping Documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html +.. _`Query Builder`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html +.. _`Doctrine Query Language`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html +.. _`Mapping Types Documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#property-mapping +.. _`Property Mapping`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#property-mapping +.. _`Reserved SQL keywords documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words +.. _`Creating Classes for the Database`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#creating-classes-for-the-database +.. _`DoctrineMongoDBBundle`: https://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html +.. _`migrations`: https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html +.. _`DoctrineFixturesBundle`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html +.. _`FrameworkExtraBundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html +.. _`newer utf8mb4 character set`: https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html +.. _`Transactions and Concurrency`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html diff --git a/doctrine/associations.rst b/doctrine/associations.rst new file mode 100644 index 00000000000..9767353f295 --- /dev/null +++ b/doctrine/associations.rst @@ -0,0 +1,415 @@ +.. index:: + single: Doctrine; Associations + +How to Work with Doctrine Associations / Relations +================================================== + +Suppose that each product in your application belongs to exactly one category. +In this case, you'll need a ``Category`` class, and a way to relate a +``Product`` object to a ``Category`` object. + +Start by creating the ``Category`` entity. Since you know that you'll eventually +need to persist category objects through Doctrine, you can let Doctrine create +the class for you. + +.. code-block:: terminal + + $ php app/console doctrine:generate:entity --no-interaction \ + --entity="AppBundle:Category" \ + --fields="name:string(255)" + +This command generates the ``Category`` entity for you, with an ``id`` field, +a ``name`` field and the associated getter and setter functions. + +Relationship Mapping Metadata +----------------------------- + +In this example, each category can be associated with *many* products, while +each product can be associated with only *one* category. This relationship +can be summarized as: *many* products to *one* category (or equivalently, +*one* category to *many* products). + +From the perspective of the ``Product`` entity, this is a many-to-one relationship. +From the perspective of the ``Category`` entity, this is a one-to-many relationship. +This is important, because the relative nature of the relationship determines +which mapping metadata to use. It also determines which class *must* hold +a reference to the other class. + +To relate the ``Product`` and ``Category`` entities, simply create a ``category`` +property on the ``Product`` class, annotated as follows: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/AppBundle/Entity/Product.php + + // ... + class Product + { + // ... + + /** + * @ORM\ManyToOne(targetEntity="Category", inversedBy="products") + * @ORM\JoinColumn(name="category_id", referencedColumnName="id") + */ + private $category; + } + + .. code-block:: yaml + + # src/AppBundle/Resources/config/doctrine/Product.orm.yml + AppBundle\Entity\Product: + type: entity + # ... + manyToOne: + category: + targetEntity: Category + inversedBy: products + joinColumn: + name: category_id + referencedColumnName: id + + .. code-block:: xml + + + + + + + + + + + + + + +This many-to-one mapping is critical. It tells Doctrine to use the ``category_id`` +column on the ``product`` table to relate each record in that table with +a record in the ``category`` table. + +Next, since a single ``Category`` object will relate to many ``Product`` +objects, a ``products`` property can be added to the ``Category`` class +to hold those associated objects. + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/AppBundle/Entity/Category.php + + // ... + use Doctrine\Common\Collections\ArrayCollection; + + class Category + { + // ... + + /** + * @ORM\OneToMany(targetEntity="Product", mappedBy="category") + */ + private $products; + + public function __construct() + { + $this->products = new ArrayCollection(); + } + } + + .. code-block:: yaml + + # src/AppBundle/Resources/config/doctrine/Category.orm.yml + AppBundle\Entity\Category: + type: entity + # ... + oneToMany: + products: + targetEntity: Product + mappedBy: category + # Don't forget to initialize the collection in + # the __construct() method of the entity + + .. code-block:: xml + + + + + + + + + + + + + +While the many-to-one mapping shown earlier was mandatory, this one-to-many +mapping is optional. It is included here to help demonstrate Doctrine's range +of relationship management capabilities. Plus, in the context of this application, +it will likely be convenient for each ``Category`` object to automatically +own a collection of its related ``Product`` objects. + +.. note:: + + The code in the constructor is important. Rather than being instantiated + as a traditional ``array``, the ``$products`` property must be of a type + that implements Doctrine's ``Collection`` interface. In this case, an + ``ArrayCollection`` object is used. This object looks and acts almost + *exactly* like an array, but has some added flexibility. If this makes + you uncomfortable, don't worry. Just imagine that it's an ``array`` + and you'll be in good shape. + +.. seealso:: + + To understand ``inversedBy`` and ``mappedBy`` usage, see Doctrine's + `Association Updates`_ documentation. + +.. tip:: + + The targetEntity value in the metadata used above can reference any entity + with a valid namespace, not just entities defined in the same namespace. To + relate to an entity defined in a different class or bundle, enter a full + namespace as the targetEntity. + +Now that you've added new properties to both the ``Product`` and ``Category`` +classes, you must generate the missing getter and setter methods manually or +using your own IDE. + +Ignore the Doctrine metadata for a moment. You now have two classes - ``Product`` +and ``Category``, with a natural many-to-one relationship. The ``Product`` +class holds a *single* ``Category`` object, and the ``Category`` class holds +a *collection* of ``Product`` objects. In other words, you've built your classes +in a way that makes sense for your application. The fact that the data needs +to be persisted to a database is always secondary. + +Now, review the metadata above the ``Product`` entity's ``$category`` property. +It tells Doctrine that the related class is ``Category``, and that the ``id`` +of the related category record should be stored in a ``category_id`` field +on the ``product`` table. + +In other words, the related ``Category`` object will be stored in the +``$category`` property, but behind the scenes, Doctrine will persist this +relationship by storing the category's id in the ``category_id`` column +of the ``product`` table. + +.. image:: /_images/doctrine/mapping_relations.png + :align: center + +The metadata above the ``Category`` entity's ``$products`` property is less +complicated. It simply tells Doctrine to look at the ``Product.category`` +property to figure out how the relationship is mapped. + +Before you continue, be sure to tell Doctrine to add the new ``category`` +table, the new ``product.category_id`` column, and the new foreign key: + +.. code-block:: terminal + + $ php app/console doctrine:schema:update --force + +Saving Related Entities +----------------------- + +Now you can see this new code in action! Imagine you're inside a controller:: + + // ... + + use AppBundle\Entity\Category; + use AppBundle\Entity\Product; + use Symfony\Component\HttpFoundation\Response; + + class DefaultController extends Controller + { + public function createProductAction() + { + $category = new Category(); + $category->setName('Computer Peripherals'); + + $product = new Product(); + $product->setName('Keyboard'); + $product->setPrice(19.99); + $product->setDescription('Ergonomic and stylish!'); + + // relates this product to the category + $product->setCategory($category); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($category); + $entityManager->persist($product); + $entityManager->flush(); + + return new Response( + 'Saved new product with id: '.$product->getId() + .' and new category with id: '.$category->getId() + ); + } + } + +Now, a single row is added to both the ``category`` and ``product`` tables. +The ``product.category_id`` column for the new product is set to whatever +the ``id`` is of the new category. Doctrine manages the persistence of this +relationship for you. + +Fetching Related Objects +------------------------ + +When you need to fetch associated objects, your workflow looks just like it +did before. First, fetch a ``$product`` object and then access its related +``Category`` object:: + + use AppBundle\Entity\Product; + // ... + + public function showAction($productId) + { + $product = $this->getDoctrine() + ->getRepository(Product::class) + ->find($productId); + + $categoryName = $product->getCategory()->getName(); + + // ... + } + +In this example, you first query for a ``Product`` object based on the product's +``id``. This issues a query for *just* the product data and hydrates the +``$product`` object with that data. Later, when you call ``$product->getCategory()->getName()``, +Doctrine silently makes a second query to find the ``Category`` that's related +to this ``Product``. It prepares the ``$category`` object and returns it to +you. + +.. image:: /_images/doctrine/mapping_relations_proxy.png + :align: center + +What's important is the fact that you have easy access to the product's related +category, but the category data isn't actually retrieved until you ask for +the category (i.e. it's "lazily loaded"). + +You can also query in the other direction:: + + public function showProductsAction($categoryId) + { + $category = $this->getDoctrine() + ->getRepository(Category::class) + ->find($categoryId); + + $products = $category->getProducts(); + + // ... + } + +In this case, the same things occur: you first query out for a single ``Category`` +object, and then Doctrine makes a second query to retrieve the related ``Product`` +objects, but only once/if you ask for them (i.e. when you call ``getProducts()``). +The ``$products`` variable is an array of all ``Product`` objects that relate +to the given ``Category`` object via their ``category_id`` value. + +.. sidebar:: Relationships and Proxy Classes + + This "lazy loading" is possible because, when necessary, Doctrine returns + a "proxy" object in place of the true object. Look again at the above + example:: + + $product = $this->getDoctrine() + ->getRepository(Product::class) + ->find($productId); + + $category = $product->getCategory(); + + // prints "Proxies\AppBundleEntityCategoryProxy" + dump(get_class($category)); + die(); + + This proxy object extends the true ``Category`` object, and looks and + acts exactly like it. The difference is that, by using a proxy object, + Doctrine can delay querying for the real ``Category`` data until you + actually need that data (e.g. until you call ``$category->getName()``). + + The proxy classes are generated by Doctrine and stored in the cache directory. + And though you'll probably never even notice that your ``$category`` + object is actually a proxy object, it's important to keep it in mind. + + In the next section, when you retrieve the product and category data + all at once (via a *join*), Doctrine will return the *true* ``Category`` + object, since nothing needs to be lazily loaded. + +Joining Related Records +----------------------- + +In the above examples, two queries were made - one for the original object +(e.g. a ``Category``) and one for the related object(s) (e.g. the ``Product`` +objects). + +.. tip:: + + Remember that you can see all of the queries made during a request via + the web debug toolbar. + +Of course, if you know up front that you'll need to access both objects, you +can avoid the second query by issuing a join in the original query. Add the +following method to the ``ProductRepository`` class:: + + // src/AppBundle/Repository/ProductRepository.php + public function findOneByIdJoinedToCategory($productId) + { + $query = $this->getEntityManager() + ->createQuery( + 'SELECT p, c FROM AppBundle:Product p + JOIN p.category c + WHERE p.id = :id' + )->setParameter('id', $productId); + + try { + return $query->getSingleResult(); + } catch (\Doctrine\ORM\NoResultException $exception) { + return null; + } + } + +Now, you can use this method in your controller to query for a ``Product`` +object and its related ``Category`` with just one query:: + + public function showAction($productId) + { + $product = $this->getDoctrine() + ->getRepository(Product::class) + ->findOneByIdJoinedToCategory($productId); + + $category = $product->getCategory(); + + // ... + } + +More Information on Associations +-------------------------------- + +This section has been an introduction to one common type of entity relationship, +the one-to-many relationship. For more advanced details and examples of how +to use other types of relations (e.g. one-to-one, many-to-many), see +Doctrine's `Association Mapping Documentation`_. + +.. note:: + + If you're using annotations, you'll need to prepend all annotations with + ``@ORM\`` (e.g. ``@ORM\OneToMany``), which is not reflected in Doctrine's + documentation. You'll also need to include the ``use Doctrine\ORM\Mapping as ORM;`` + statement, which *imports* the ``ORM`` annotations prefix. + +.. _`Association Mapping Documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html +.. _`Association Updates`: http://docs.doctrine-project.org/en/latest/reference/unitofwork-associations.html diff --git a/cookbook/doctrine/common_extensions.rst b/doctrine/common_extensions.rst similarity index 90% rename from cookbook/doctrine/common_extensions.rst rename to doctrine/common_extensions.rst index 0944fa5e7cb..3a3f559fc81 100644 --- a/cookbook/doctrine/common_extensions.rst +++ b/doctrine/common_extensions.rst @@ -14,7 +14,7 @@ functionality for `Sluggable`_, `Translatable`_, `Timestampable`_, `Loggable`_, The usage for each of these extensions is explained in that repository. However, to install/activate each extension you must register and activate an -:doc:`Event Listener `. +:doc:`Event Listener `. To do this, you have two options: #. Use the `StofDoctrineExtensionsBundle`_, which integrates the above library. @@ -23,7 +23,7 @@ To do this, you have two options: with Symfony: `Install Gedmo Doctrine2 extensions in Symfony2`_ .. _`DoctrineExtensions`: https://github.com/Atlantic18/DoctrineExtensions -.. _`StofDoctrineExtensionsBundle`: https://github.com/stof/StofDoctrineExtensionsBundle +.. _`StofDoctrineExtensionsBundle`: https://symfony.com/doc/master/bundles/StofDoctrineExtensionsBundle/index.html .. _`Sluggable`: https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/sluggable.md .. _`Translatable`: https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/translatable.md .. _`Timestampable`: https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/timestampable.md diff --git a/cookbook/doctrine/console.rst b/doctrine/console.rst similarity index 87% rename from cookbook/doctrine/console.rst rename to doctrine/console.rst index 0cfb7befca0..84d1cca146d 100644 --- a/cookbook/doctrine/console.rst +++ b/doctrine/console.rst @@ -9,32 +9,32 @@ The Doctrine2 ORM integration offers several console commands under the ``doctrine`` namespace. To view the command list you can use the ``list`` command: -.. code-block:: bash +.. code-block:: terminal $ php app/console list doctrine A list of available commands will print out. You can find out more information about any of these commands (or any Symfony command) by running the ``help`` command. For example, to get details about the ``doctrine:database:create`` -task, run: +command, run: -.. code-block:: bash +.. code-block:: terminal $ php app/console help doctrine:database:create -Some notable or interesting tasks include: +Some notable or interesting commands include: * ``doctrine:ensure-production-settings`` - checks to see if the current environment is configured efficiently for production. This should always be run in the ``prod`` environment: - .. code-block:: bash + .. code-block:: terminal $ php app/console doctrine:ensure-production-settings --env=prod * ``doctrine:mapping:import`` - allows Doctrine to introspect an existing database and create mapping information. For more information, see - :doc:`/cookbook/doctrine/reverse_engineering`. + :doc:`/doctrine/reverse_engineering`. * ``doctrine:mapping:info`` - tells you all of the entities that Doctrine is aware of and whether or not there are any basic errors with the mapping. diff --git a/doctrine/custom_dql_functions.rst b/doctrine/custom_dql_functions.rst new file mode 100644 index 00000000000..bc9687c065a --- /dev/null +++ b/doctrine/custom_dql_functions.rst @@ -0,0 +1,151 @@ +.. index:: + single: Doctrine; Custom DQL functions + +How to Register custom DQL Functions +==================================== + +Doctrine allows you to specify custom DQL functions. For more information +on this topic, read Doctrine's cookbook article "`DQL User Defined Functions`_". + +In Symfony, you can register your custom DQL functions as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine: + orm: + # ... + dql: + string_functions: + test_string: AppBundle\DQL\StringFunction + second_string: AppBundle\DQL\SecondStringFunction + numeric_functions: + test_numeric: AppBundle\DQL\NumericFunction + datetime_functions: + test_datetime: AppBundle\DQL\DatetimeFunction + + .. code-block:: xml + + + + + + + + + AppBundle\DQL\StringFunction + AppBundle\DQL\SecondStringFunction + AppBundle\DQL\NumericFunction + AppBundle\DQL\DatetimeFunction + + + + + + .. code-block:: php + + // app/config/config.php + use AppBundle\DQL\StringFunction; + use AppBundle\DQL\SecondStringFunction; + use AppBundle\DQL\NumericFunction; + use AppBundle\DQL\DatetimeFunction; + + $container->loadFromExtension('doctrine', array( + 'orm' => array( + // ... + 'dql' => array( + 'string_functions' => array( + 'test_string' => StringFunction::class, + 'second_string' => SecondStringFunction::class, + ), + 'numeric_functions' => array( + 'test_numeric' => NumericFunction::class, + ), + 'datetime_functions' => array( + 'test_datetime' => DatetimeFunction::class, + ), + ), + ), + )); + +.. note:: + + In case the ``entity_managers`` were named explicitly, configuring the functions with the + orm directly will trigger the exception `Unrecognized option "dql" under "doctrine.orm"`. + The ``dql`` configuration block must be defined under the named entity manager. + + .. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine: + orm: + # ... + entity_managers: + example_manager: + # Place your functions here + dql: + datetime_functions: + test_datetime: AppBundle\DQL\DatetimeFunction + + .. code-block:: xml + + # app/config/config.xml + + + + + + + + + + + + AppBundle\DQL\DatetimeFunction + + + + + + + + .. code-block:: php + + // app/config/config.php + use AppBundle\DQL\DatetimeFunction; + + $container->loadFromExtension('doctrine', array( + 'doctrine' => array( + 'orm' => array( + // ... + 'entity_managers' => array( + 'example_manager' => array( + // place your functions here + 'dql' => array( + 'datetime_functions' => array( + 'test_datetime' => DatetimeFunction::class, + ), + ), + ), + ), + ), + ), + )); + +.. _`DQL User Defined Functions`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/dql-user-defined-functions.html diff --git a/cookbook/doctrine/dbal.rst b/doctrine/dbal.rst similarity index 74% rename from cookbook/doctrine/dbal.rst rename to doctrine/dbal.rst index 006c99371ef..37283f6dcdb 100644 --- a/cookbook/doctrine/dbal.rst +++ b/doctrine/dbal.rst @@ -9,7 +9,7 @@ How to Use Doctrine DBAL This article is about the Doctrine DBAL. Typically, you'll work with the higher level Doctrine ORM layer, which simply uses the DBAL behind the scenes to actually communicate with the database. To read more about - the Doctrine ORM, see ":doc:`/book/doctrine`". + the Doctrine ORM, see ":doc:`/doctrine`". The `Doctrine`_ Database Abstraction Layer (DBAL) is an abstraction layer that sits on top of `PDO`_ and offers an intuitive and flexible API for communicating @@ -40,17 +40,28 @@ To get started, configure the database connection parameters: .. code-block:: xml - - - + + + + + + + + .. code-block:: php @@ -76,8 +87,8 @@ You can then access the Doctrine DBAL connection by accessing the { public function indexAction() { - $conn = $this->get('database_connection'); - $users = $conn->fetchAll('SELECT * FROM users'); + $connection = $this->get('database_connection'); + $users = $connection->fetchAll('SELECT * FROM users'); // ... } @@ -107,8 +118,10 @@ mapping types, read Doctrine's `Custom Mapping Types`_ section of their document + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/doctrine + http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> @@ -121,11 +134,14 @@ mapping types, read Doctrine's `Custom Mapping Types`_ section of their document .. code-block:: php // app/config/config.php + use AppBundle\Type\CustomFirst; + use AppBundle\Type\CustomSecond; + $container->loadFromExtension('doctrine', array( 'dbal' => array( 'types' => array( - 'custom_first' => 'AppBundle\Type\CustomFirst', - 'custom_second' => 'AppBundle\Type\CustomSecond', + 'custom_first' => CustomFirst::class, + 'custom_second' => CustomSecond::class, ), ), )); @@ -156,8 +172,10 @@ mapping type: + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/doctrine + http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> @@ -177,7 +195,7 @@ mapping type: ), )); -.. _`PDO`: http://www.php.net/pdo +.. _`PDO`: https://php.net/pdo .. _`Doctrine`: http://www.doctrine-project.org .. _`DBAL Documentation`: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/index.html .. _`Custom Mapping Types`: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types diff --git a/cookbook/doctrine/event_listeners_subscribers.rst b/doctrine/event_listeners_subscribers.rst similarity index 60% rename from cookbook/doctrine/event_listeners_subscribers.rst rename to doctrine/event_listeners_subscribers.rst index bd1f322d0b3..716ff97a3b8 100644 --- a/cookbook/doctrine/event_listeners_subscribers.rst +++ b/doctrine/event_listeners_subscribers.rst @@ -2,14 +2,15 @@ single: Doctrine; Event listeners and subscribers .. _doctrine-event-config: +.. _how-to-register-event-listeners-and-subscribers: -How to Register Event Listeners and Subscribers -=============================================== +Doctrine Event Listeners and Subscribers +======================================== -Doctrine packages a rich event system that fires events when almost anything +Doctrine packages have a rich event system that fires events when almost anything happens inside the system. For you, this means that you can create arbitrary -:doc:`services ` and tell Doctrine to notify those -objects whenever a certain action (e.g. ``prePersist``) happens within Doctrine. +:doc:`services ` and tell Doctrine to notify those +objects whenever a certain action (e.g. ``prePersist()``) happens within Doctrine. This could be useful, for example, to create an independent search index whenever an object in your database is saved. @@ -23,7 +24,7 @@ Configuring the Listener/Subscriber ----------------------------------- To register a service to act as an event listener or subscriber you just have -to :ref:`tag ` it with the appropriate name. Depending +to :doc:`tag ` it with the appropriate name. Depending on your use-case, you can hook a listener into every DBAL connection and ORM entity manager or just into one specific DBAL connection and all the entity managers that use this connection. @@ -42,15 +43,15 @@ managers that use this connection. services: my.listener: - class: Acme\SearchBundle\EventListener\SearchIndexer + class: AppBundle\EventListener\SearchIndexer tags: - { name: doctrine.event_listener, event: postPersist } my.listener2: - class: Acme\SearchBundle\EventListener\SearchIndexer2 + class: AppBundle\EventListener\SearchIndexer2 tags: - { name: doctrine.event_listener, event: postPersist, connection: default } my.subscriber: - class: Acme\SearchBundle\EventListener\SearchIndexerSubscriber + class: AppBundle\EventListener\SearchIndexerSubscriber tags: - { name: doctrine.event_subscriber, connection: default } @@ -67,13 +68,13 @@ managers that use this connection. - + - + - + @@ -81,7 +82,9 @@ managers that use this connection. .. code-block:: php - use Symfony\Component\DependencyInjection\Definition; + use AppBundle\EventListener\SearchIndexer; + use AppBundle\EventListener\SearchIndexer2; + use AppBundle\EventListener\SearchIndexerSubscriber; $container->loadFromExtension('doctrine', array( 'dbal' => array( @@ -96,24 +99,18 @@ managers that use this connection. )); $container - ->setDefinition( - 'my.listener', - new Definition('Acme\SearchBundle\EventListener\SearchIndexer') - ) + ->register('my.listener', SearchIndexer::class) ->addTag('doctrine.event_listener', array('event' => 'postPersist')) ; $container - ->setDefinition( - 'my.listener2', - new Definition('Acme\SearchBundle\EventListener\SearchIndexer2') - ) - ->addTag('doctrine.event_listener', array('event' => 'postPersist', 'connection' => 'default')) + ->register('my.listener2', SearchIndexer2::class) + ->addTag('doctrine.event_listener', array( + 'event' => 'postPersist', + 'connection' => 'default', + )) ; $container - ->setDefinition( - 'my.subscriber', - new Definition('Acme\SearchBundle\EventListener\SearchIndexerSubscriber') - ) + ->register('my.subscriber', SearchIndexerSubscriber::class) ->addTag('doctrine.event_subscriber', array('connection' => 'default')) ; @@ -122,25 +119,27 @@ Creating the Listener Class In the previous example, a service ``my.listener`` was configured as a Doctrine listener on the event ``postPersist``. The class behind that service must have -a ``postPersist`` method, which will be called when the event is dispatched:: +a ``postPersist()`` method, which will be called when the event is dispatched:: - // src/Acme/SearchBundle/EventListener/SearchIndexer.php - namespace Acme\SearchBundle\EventListener; + // src/AppBundle/EventListener/SearchIndexer.php + namespace AppBundle\EventListener; use Doctrine\ORM\Event\LifecycleEventArgs; - use Acme\StoreBundle\Entity\Product; + use AppBundle\Entity\Product; class SearchIndexer { public function postPersist(LifecycleEventArgs $args) { $entity = $args->getEntity(); - $entityManager = $args->getEntityManager(); - // perhaps you only want to act on some "Product" entity - if ($entity instanceof Product) { - // ... do something with the Product + // only act on some "Product" entity + if (!$entity instanceof Product) { + return; } + + $entityManager = $args->getEntityManager(); + // ... do something with the Product } } @@ -166,13 +165,13 @@ Creating the Subscriber Class A Doctrine event subscriber must implement the ``Doctrine\Common\EventSubscriber`` interface and have an event method for each event it subscribes to:: - // src/Acme/SearchBundle/EventListener/SearchIndexerSubscriber.php - namespace Acme\SearchBundle\EventListener; + // src/AppBundle/EventListener/SearchIndexerSubscriber.php + namespace AppBundle\EventListener; use Doctrine\Common\EventSubscriber; - use Doctrine\ORM\Event\LifecycleEventArgs; - // for Doctrine 2.4: Doctrine\Common\Persistence\Event\LifecycleEventArgs; - use Acme\StoreBundle\Entity\Product; + // for Doctrine < 2.4: use Doctrine\ORM\Event\LifecycleEventArgs; + use Doctrine\Common\Persistence\Event\LifecycleEventArgs; + use AppBundle\Entity\Product; class SearchIndexerSubscriber implements EventSubscriber { @@ -197,10 +196,10 @@ interface and have an event method for each event it subscribes to:: public function index(LifecycleEventArgs $args) { $entity = $args->getEntity(); - $entityManager = $args->getEntityManager(); // perhaps you only want to act on some "Product" entity if ($entity instanceof Product) { + $entityManager = $args->getEntityManager(); // ... do something with the Product } } @@ -208,7 +207,7 @@ interface and have an event method for each event it subscribes to:: .. tip:: - Doctrine event subscribers can not return a flexible array of methods to + Doctrine event subscribers cannot return a flexible array of methods to call for the events like the :ref:`Symfony event subscriber ` can. Doctrine event subscribers must return a simple array of the event names they subscribe to. Doctrine will then expect methods on the subscriber @@ -216,5 +215,58 @@ interface and have an event method for each event it subscribes to:: For a full reference, see chapter `The Event System`_ in the Doctrine documentation. +Lazy loading for Event Listeners +-------------------------------- + +One subtle difference between listeners and subscribers is that Symfony can load +entity listeners lazily. This means that your listener class will only be fetched +from the service container (and thus be instantiated) once the event it is linked +to actually fires. + +Lazy loading might give you a slight performance improvement when your listener +runs for events that rarely fire. Also, it can help you when you run into +*circular dependency issues* that may occur when your listener service in turn +depends on the DBAL connection. + +To mark a listener service as lazily loaded, just add the ``lazy`` attribute +to the tag like so: + +.. configuration-block:: + + .. code-block:: yaml + + services: + my.listener: + class: AppBundle\EventListener\SearchIndexer + tags: + - { name: doctrine.event_listener, event: postPersist, lazy: true } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + use AppBundle\EventListener\SearchIndexer; + + $container + ->register('my.listener', SearchIndexer::class) + ->addTag('doctrine.event_listener', array('event' => 'postPersist', 'lazy' => 'true')) + ; + +.. note:: + +   Marking an event listener as ``lazy`` has nothing to do with lazy service + definitions which are described :doc:`in their own section ` + .. _`The Event System`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html .. _`the Doctrine Documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#entity-listeners diff --git a/doctrine/lifecycle_callbacks.rst b/doctrine/lifecycle_callbacks.rst new file mode 100644 index 00000000000..a6535a9c749 --- /dev/null +++ b/doctrine/lifecycle_callbacks.rst @@ -0,0 +1,96 @@ +.. index:: + single: Doctrine; Lifecycle Callbacks + +How to Work with Lifecycle Callbacks +==================================== + +Sometimes, you need to perform an action right before or after an entity +is inserted, updated, or deleted. These types of actions are known as "lifecycle" +callbacks, as they're callback methods that you need to execute during different +stages of the lifecycle of an entity (e.g. the entity is inserted, updated, +deleted, etc). + +If you're using annotations for your metadata, start by enabling the lifecycle +callbacks. This is not necessary if you're using YAML or XML for your mapping. + +.. code-block:: php-annotations + + /** + * @ORM\Entity() + * @ORM\HasLifecycleCallbacks() + */ + class Product + { + // ... + } + +Now, you can tell Doctrine to execute a method on any of the available lifecycle +events. For example, suppose you want to set a ``createdAt`` date column to +the current date, only when the entity is first persisted (i.e. inserted): + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/AppBundle/Entity/Product.php + + /** + * @ORM\PrePersist + */ + public function setCreatedAtValue() + { + $this->createdAt = new \DateTime(); + } + + .. code-block:: yaml + + # src/AppBundle/Resources/config/doctrine/Product.orm.yml + AppBundle\Entity\Product: + type: entity + # ... + lifecycleCallbacks: + prePersist: [setCreatedAtValue] + + .. code-block:: xml + + + + + + + + + + + + + +.. note:: + + The above example assumes that you've created and mapped a ``createdAt`` + property (not shown here). + +Now, right before the entity is first persisted, Doctrine will automatically +call this method and the ``createdAt`` field will be set to the current date. + +There are several other lifecycle events that you can hook into. For more +information on other lifecycle events and lifecycle callbacks in general, see +Doctrine's `Lifecycle Events documentation`_. + +.. sidebar:: Lifecycle Callbacks and Event Listeners + + Notice that the ``setCreatedAtValue()`` method receives no arguments. This + is always the case for lifecycle callbacks and is intentional: lifecycle + callbacks should be simple methods that are concerned with internally + transforming data in the entity (e.g. setting a created/updated field, + generating a slug value). + + If you need to do some heavier lifting - like performing logging or sending + an email - you should register an external class as an event listener + or subscriber and give it access to whatever resources you need. For + more information, see :doc:`/doctrine/event_listeners_subscribers`. + +.. _`Lifecycle Events documentation`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events diff --git a/cookbook/doctrine/mapping_model_classes.rst b/doctrine/mapping_model_classes.rst similarity index 80% rename from cookbook/doctrine/mapping_model_classes.rst rename to doctrine/mapping_model_classes.rst index acace8725f2..4eadefb5206 100644 --- a/cookbook/doctrine/mapping_model_classes.rst +++ b/doctrine/mapping_model_classes.rst @@ -26,7 +26,7 @@ register the mappings for your model classes. .. versionadded:: 2.6 Support for defining namespace aliases was introduced in Symfony 2.6. It is safe to define the aliases with older versions of Symfony as - the aliases are the last argument to ``createXmlMappingDriver`` and + the aliases are the last argument to ``createXmlMappingDriver()`` and are ignored by PHP if that argument doesn't exist. In your bundle class, write the following code to register the compiler pass. @@ -37,6 +37,7 @@ be adapted for your case:: use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\DoctrineMongoDBMappingsPass; use Doctrine\Bundle\CouchDBBundle\DependencyInjection\Compiler\DoctrineCouchDBMappingsPass; use Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\DoctrinePhpcrMappingsPass; + use Symfony\Cmf\RoutingBundle\Model; class CmfRoutingBundle extends Bundle { @@ -45,52 +46,48 @@ be adapted for your case:: parent::build($container); // ... - $modelDir = realpath(__DIR__.'/Resources/config/doctrine/model'); + $modelDirectory = realpath(__DIR__.'/Resources/config/doctrine/model'); $mappings = array( - $modelDir => 'Symfony\Cmf\RoutingBundle\Model', + $modelDirectory => Model::class, ); - $ormCompilerClass = 'Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass'; - if (class_exists($ormCompilerClass)) { + if (class_exists(DoctrineOrmMappingsPass::class)) { $container->addCompilerPass( DoctrineOrmMappingsPass::createXmlMappingDriver( $mappings, array('cmf_routing.model_manager_name'), 'cmf_routing.backend_type_orm', - array('CmfRoutingBundle' => 'Symfony\Cmf\RoutingBundle\Model') + array('CmfRoutingBundle' => Model::class) )); } - $mongoCompilerClass = 'Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\DoctrineMongoDBMappingsPass'; - if (class_exists($mongoCompilerClass)) { + if (class_exists(DoctrineMongoDBMappingsPass::class)) { $container->addCompilerPass( DoctrineMongoDBMappingsPass::createXmlMappingDriver( $mappings, array('cmf_routing.model_manager_name'), 'cmf_routing.backend_type_mongodb', - array('CmfRoutingBundle' => 'Symfony\Cmf\RoutingBundle\Model') + array('CmfRoutingBundle' => Model::class) )); } - $couchCompilerClass = 'Doctrine\Bundle\CouchDBBundle\DependencyInjection\Compiler\DoctrineCouchDBMappingsPass'; - if (class_exists($couchCompilerClass)) { + if (class_exists(DoctrineCouchDBMappingsPass::class)) { $container->addCompilerPass( DoctrineCouchDBMappingsPass::createXmlMappingDriver( $mappings, array('cmf_routing.model_manager_name'), 'cmf_routing.backend_type_couchdb', - array('CmfRoutingBundle' => 'Symfony\Cmf\RoutingBundle\Model') + array('CmfRoutingBundle' => Model::class) )); } - $phpcrCompilerClass = 'Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\DoctrinePhpcrMappingsPass'; - if (class_exists($phpcrCompilerClass)) { + if (class_exists(DoctrinePhpcrMappingsPass::class)) { $container->addCompilerPass( DoctrinePhpcrMappingsPass::createXmlMappingDriver( $mappings, array('cmf_routing.model_manager_name'), 'cmf_routing.backend_type_phpcr', - array('CmfRoutingBundle' => 'Symfony\Cmf\RoutingBundle\Model') + array('CmfRoutingBundle' => Model::class) )); } } @@ -131,15 +128,22 @@ Annotations, XML, Yaml, PHP and StaticPHP. The arguments are: ``DoctrineOrmMappingsPass`` and adapted to use the ``DefaultFileLocator`` instead of the ``SymfonyFileLocator``:: + use Doctrine\Common\Persistence\Mapping\Driver\DefaultFileLocator; + use Doctrine\ORM\Mapping\Driver\XmlDriver; + use AppBundle\Model; + + // ... private function buildMappingCompilerPass() { - $arguments = array(array(realpath(__DIR__ . '/Resources/config/doctrine-base')), '.orm.xml'); - $locator = new Definition('Doctrine\Common\Persistence\Mapping\Driver\DefaultFileLocator', $arguments); - $driver = new Definition('Doctrine\ORM\Mapping\Driver\XmlDriver', array($locator)); + $fileLocator = new Definition(DefaultFileLocator::class, array( + array(realpath(__DIR__ . '/Resources/config/doctrine-base')), + '.orm.xml' + )); + $driver = new Definition(XmlDriver::class, array($fileLocator)); return new DoctrineOrmMappingsPass( $driver, - array('Full\Namespace'), + array(Model::class), array('your_bundle.manager_name'), 'your_bundle.orm_enabled' ); diff --git a/cookbook/configuration/mongodb_session_storage.rst b/doctrine/mongodb_session_storage.rst similarity index 85% rename from cookbook/configuration/mongodb_session_storage.rst rename to doctrine/mongodb_session_storage.rst index c2f912e8b9e..fcfecfe3dd2 100644 --- a/cookbook/configuration/mongodb_session_storage.rst +++ b/doctrine/mongodb_session_storage.rst @@ -30,12 +30,12 @@ need to change/add some parameters in the main configuration file: mongo_client: class: MongoClient # if using a username and password - arguments: [mongodb://%mongodb_username%:%mongodb_password%@%mongodb_host%:27017] + arguments: ['mongodb://%mongodb_username%:%mongodb_password%@%mongodb_host%:27017'] # if not using a username and password - arguments: [mongodb://%mongodb_host%:27017] + arguments: ['mongodb://%mongodb_host%:27017'] session.handler.mongo: class: Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler - arguments: [@mongo_client, %mongo.session.options%] + arguments: ['@mongo_client', '%mongo.session.options%'] .. code-block:: xml @@ -77,7 +77,7 @@ need to change/add some parameters in the main configuration file: .. code-block:: php use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler; $container->loadFromExtension('framework', array( 'session' => array( @@ -88,17 +88,19 @@ need to change/add some parameters in the main configuration file: ), )); - $container->setDefinition('mongo_client', new Definition('MongoClient', array( - // if using a username and password - array('mongodb://%mongodb_username%:%mongodb_password%@%mongodb_host%:27017'), - // if not using a username and password - array('mongodb://%mongodb_host%:27017'), - ))); + $container->register('mongo_client', \MongoClient::class) + ->setArguments(array( + // if using a username and password + array('mongodb://%mongodb_username%:%mongodb_password%@%mongodb_host%:27017'), + // if not using a username and password + array('mongodb://%mongodb_host%:27017'), + )); - $container->setDefinition('session.handler.mongo', new Definition( - 'Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler', - array(new Reference('mongo_client'), '%mongo.session.options%') - )); + $container->register('session.handler.mongo', MongoDbSessionHandler::class) + ->setArguments(array( + new Reference('mongo_client'), + '%mongo.session.options%', + )); The parameters used above should be defined somewhere in your application, often in your main parameters configuration: @@ -145,7 +147,6 @@ parameters configuration: .. code-block:: php use Symfony\Component\DependencyInjection\Reference; - use Symfony\Component\DependencyInjection\Definition; $container->setParameter('mongo.session.options', array( 'database' => 'session_db', // your MongoDB database name @@ -162,10 +163,10 @@ Because MongoDB uses dynamic collection schemas, you do not need to do anything session collection. However, you may want to add an index to improve garbage collection performance. From the `MongoDB shell`_: -.. code-block:: sql +.. code-block:: javascript use session_db - db.session.ensureIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } ) + db.session.ensureIndex( { "expires_at": 1 }, { expireAfterSeconds: 0 } ) .. _installed and configured a MongoDB server: http://docs.mongodb.org/manual/installation/ -.. _MongoDB shell: http://docs.mongodb.org/v2.2/tutorial/getting-started-with-the-mongo-shell/ \ No newline at end of file +.. _MongoDB shell: http://docs.mongodb.org/v2.2/tutorial/getting-started-with-the-mongo-shell/ diff --git a/cookbook/doctrine/multiple_entity_managers.rst b/doctrine/multiple_entity_managers.rst similarity index 68% rename from cookbook/doctrine/multiple_entity_managers.rst rename to doctrine/multiple_entity_managers.rst index 5434d847365..d1624a9b307 100644 --- a/cookbook/doctrine/multiple_entity_managers.rst +++ b/doctrine/multiple_entity_managers.rst @@ -16,6 +16,12 @@ entity manager that connects to another database might handle the rest. usually required. Be sure you actually need multiple entity managers before adding in this layer of complexity. +.. caution:: + + Entities cannot define associations across different entity managers. If you + need that, there are `several alternatives `_ + that require some custom setup. + The following configuration code shows how you can configure two entity managers: .. configuration-block:: @@ -27,20 +33,20 @@ The following configuration code shows how you can configure two entity managers default_connection: default connections: default: - driver: "%database_driver%" - host: "%database_host%" - port: "%database_port%" - dbname: "%database_name%" - user: "%database_user%" - password: "%database_password%" + driver: pdo_mysql + host: '%database_host%' + port: '%database_port%' + dbname: '%database_name%' + user: '%database_user%' + password: '%database_password%' charset: UTF8 customer: - driver: "%database_driver2%" - host: "%database_host2%" - port: "%database_port2%" - dbname: "%database_name2%" - user: "%database_user2%" - password: "%database_password2%" + driver: pdo_mysql + host: '%database_host2%' + port: '%database_port2%' + dbname: '%database_name2%' + user: '%database_user2%' + password: '%database_password2%' charset: UTF8 orm: @@ -59,16 +65,18 @@ The following configuration code shows how you can configure two entity managers .. code-block:: xml - - - - - + + + + - - + - - - - - + + + + + - - - - - - + + + + + + .. code-block:: php @@ -108,7 +116,7 @@ The following configuration code shows how you can configure two entity managers 'default_connection' => 'default', 'connections' => array( 'default' => array( - 'driver' => '%database_driver%', + 'driver' => 'pdo_mysql', 'host' => '%database_host%', 'port' => '%database_port%', 'dbname' => '%database_name%', @@ -117,7 +125,7 @@ The following configuration code shows how you can configure two entity managers 'charset' => 'UTF8', ), 'customer' => array( - 'driver' => '%database_driver2%', + 'driver' => 'pdo_mysql', 'host' => '%database_host2%', 'port' => '%database_port2%', 'dbname' => '%database_name2%', @@ -162,7 +170,7 @@ for each entity manager. When working with multiple connections to create your databases: -.. code-block:: bash +.. code-block:: terminal # Play only with "default" connection $ php app/console doctrine:database:create @@ -172,7 +180,7 @@ When working with multiple connections to create your databases: When working with multiple entity managers to update your schema: -.. code-block:: bash +.. code-block:: terminal # Play only with "default" mappings $ php app/console doctrine:schema:update --force @@ -188,13 +196,13 @@ the default entity manager (i.e. ``default``) is returned:: public function indexAction() { // All three return the "default" entity manager - $em = $this->get('doctrine')->getManager(); - $em = $this->get('doctrine')->getManager('default'); - $em = $this->get('doctrine.orm.default_entity_manager'); + $entityManager = $this->get('doctrine')->getManager(); + $entityManager = $this->get('doctrine')->getManager('default'); + $entityManager = $this->get('doctrine.orm.default_entity_manager'); // Both of these return the "customer" entity manager - $customerEm = $this->get('doctrine')->getManager('customer'); - $customerEm = $this->get('doctrine.orm.customer_entity_manager'); + $customerEntityManager = $this->get('doctrine')->getManager('customer'); + $customerEntityManager = $this->get('doctrine.orm.customer_entity_manager'); } } @@ -204,27 +212,29 @@ entity manager to persist and fetch its entities. The same applies to repository calls:: + use AcmeStoreBundle\Entity\Customer; + use AcmeStoreBundle\Entity\Product; + class UserController extends Controller { public function indexAction() { // Retrieves a repository managed by the "default" em $products = $this->get('doctrine') - ->getRepository('AcmeStoreBundle:Product') + ->getRepository(Product::class) ->findAll() ; // Explicit way to deal with the "default" em $products = $this->get('doctrine') - ->getRepository('AcmeStoreBundle:Product', 'default') + ->getRepository(Product::class, 'default') ->findAll() ; // Retrieves a repository managed by the "customer" em $customers = $this->get('doctrine') - ->getRepository('AcmeCustomerBundle:Customer', 'customer') + ->getRepository(Customer::class, 'customer') ->findAll() ; } } - diff --git a/cookbook/configuration/pdo_session_storage.rst b/doctrine/pdo_session_storage.rst similarity index 58% rename from cookbook/configuration/pdo_session_storage.rst rename to doctrine/pdo_session_storage.rst index 90789b119e8..1edaf2db8b7 100644 --- a/cookbook/configuration/pdo_session_storage.rst +++ b/doctrine/pdo_session_storage.rst @@ -34,45 +34,55 @@ To use it, you just need to change some parameters in the main configuration fil class: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler public: false arguments: - - "mysql:dbname=mydatabase" + - 'mysql:dbname=mydatabase' - { db_username: myuser, db_password: mypassword } .. code-block:: xml - - - - - - - mysql:dbname=mydatabase - - myuser - mypassword - - - + + + + + + + + + + mysql:dbname=mydatabase + + myuser + mypassword + + + + .. code-block:: php // app/config/config.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; + use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; + + // ... $container->loadFromExtension('framework', array( - ..., + // ... 'session' => array( - // ..., + // ... 'handler_id' => 'session.handler.pdo', ), )); - $storageDefinition = new Definition('Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler', array( - 'mysql:dbname=mydatabase', - array('db_username' => 'myuser', 'db_password' => 'mypassword') - )); - $container->setDefinition('session.handler.pdo', $storageDefinition); + $container->register('session.handler.pdo', PdoSessionHandler::class) + ->setArguments(array( + 'mysql:dbname=mydatabase', + array('db_username' => 'myuser', 'db_password' => 'mypassword'), + )); Configuring the Table and Column Names -------------------------------------- @@ -92,41 +102,48 @@ a second array argument to ``PdoSessionHandler``: class: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler public: false arguments: - - "mysql:dbname=mydatabase" + - 'mysql:dbname=mydatabase' - { db_table: sessions, db_username: myuser, db_password: mypassword } .. code-block:: xml - - - mysql:dbname=mydatabase - - sessions - myuser - mypassword - - - + + + + + + mysql:dbname=mydatabase + + sessions + myuser + mypassword + + + + .. code-block:: php // app/config/config.php - use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; // ... - $storageDefinition = new Definition('Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler', array( - 'mysql:dbname=mydatabase', - array('db_table' => 'sessions', 'db_username' => 'myuser', 'db_password' => 'mypassword') - )); - $container->setDefinition('session.handler.pdo', $storageDefinition); + $container->register('session.handler.pdo', PdoSessionHandler::class) + ->setArguments(array( + 'mysql:dbname=mydatabase', + array('db_table' => 'sessions', 'db_username' => 'myuser', 'db_password' => 'mypassword'), + )); .. versionadded:: 2.6 The ``db_lifetime_col`` was introduced in Symfony 2.6. Prior to 2.6, this column did not exist. -These are parameters that you must configure: +These are parameters that you can configure: ``db_table`` (default ``sessions``): The name of the session table in your database; @@ -143,7 +160,6 @@ These are parameters that you must configure: ``db_lifetime_col`` (default ``sess_lifetime``): The name of the lifetime column in your session table (INTEGER). - Sharing your Database Connection Information -------------------------------------------- @@ -164,22 +180,32 @@ of your project's data, you can use the connection settings from the class: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler public: false arguments: - - "mysql:host=%database_host%;port=%database_port%;dbname=%database_name%" - - { db_username: %database_user%, db_password: %database_password% } + - 'mysql:host=%database_host%;port=%database_port%;dbname=%database_name%' + - { db_username: '%database_user%', db_password: '%database_password%' } .. code-block:: xml - - mysql:host=%database_host%;port=%database_port%;dbname=%database_name% - - %database_user% - %database_password% - - + + + + + + mysql:host=%database_host%;port=%database_port%;dbname=%database_name% + + %database_user% + %database_password% + + + + .. code-block:: php - $storageDefinition = new Definition('Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler', array( + // ... + $storageDefinition = new Definition(PdoSessionHandler::class, array( 'mysql:host=%database_host%;port=%database_port%;dbname=%database_name%', array('db_username' => '%database_user%', 'db_password' => '%database_password%') )); @@ -190,8 +216,18 @@ Preparing the Database to Store Sessions ---------------------------------------- Before storing sessions in the database, you must create the table that stores -the information. The following sections contain some examples of the SQL statements -you may use for your specific database engine. +the information. The session handler provides a method called +:method:`Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler::createTable` +to set up this table for you according to the database engine used:: + + try { + $sessionHandlerService->createTable(); + } catch (\PDOException $exception) { + // the table could not be created for some reason + } + +If you prefer to set up the table yourself, these are some examples of the SQL +statements you may use according to your specific database engine. .. _pdo-session-handle-26-changes: @@ -217,7 +253,7 @@ MySQL .. code-block:: sql CREATE TABLE `sessions` ( - `sess_id` VARBINARY(128) NOT NULL PRIMARY KEY, + `sess_id` VARCHAR(128) NOT NULL PRIMARY KEY, `sess_data` BLOB NOT NULL, `sess_time` INTEGER UNSIGNED NOT NULL, `sess_lifetime` MEDIUMINT NOT NULL @@ -272,6 +308,6 @@ Microsoft SQL Server If the application stores large amounts of session data, this problem can be solved by increasing the column size (use ``BLOB`` or even ``MEDIUMBLOB``). When using MySQL as the database engine, you can also enable the `strict SQL mode`_ - to get noticed when such an error happens. + to be notified when such an error happens. .. _`strict SQL mode`: https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst new file mode 100644 index 00000000000..236928ffe1b --- /dev/null +++ b/doctrine/registration_form.rst @@ -0,0 +1,471 @@ +.. index:: + single: Doctrine; Simple Registration Form + single: Form; Simple Registration Form + single: Security; Simple Registration Form + +How to Implement a Simple Registration Form +=========================================== + +Creating a registration form is pretty easy - it *really* means just creating +a form that will update some ``User`` model object (a Doctrine entity in this +example) and then save it. + +.. tip:: + + The popular `FOSUserBundle`_ provides a registration form, reset password + form and other user management functionality. + +If you don't already have a ``User`` entity and a working login system, +first start with :doc:`/security/entity_provider`. + +Your ``User`` entity will probably at least have the following fields: + +``username`` + This will be used for logging in, unless you instead want your user to + :ref:`login via email ` (in that case, this + field is unnecessary). + +``email`` + A nice piece of information to collect. You can also allow users to + :ref:`login via email `. + +``password`` + The encoded password. + +``plainPassword`` + This field is *not* persisted: (notice no ``@ORM\Column`` above it). It + temporarily stores the plain password from the registration form. This field + can be validated and is then used to populate the ``password`` field. + +With some validation added, your class may look something like this:: + + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; + use Symfony\Component\Security\Core\User\UserInterface; + + /** + * @ORM\Entity + * @UniqueEntity(fields="email", message="Email already taken") + * @UniqueEntity(fields="username", message="Username already taken") + */ + class User implements UserInterface + { + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @ORM\Column(type="string", length=255, unique=true) + * @Assert\NotBlank() + * @Assert\Email() + */ + private $email; + + /** + * @ORM\Column(type="string", length=255, unique=true) + * @Assert\NotBlank() + */ + private $username; + + /** + * @Assert\NotBlank() + * @Assert\Length(max=4096) + */ + private $plainPassword; + + /** + * The below length depends on the "algorithm" you use for encoding + * the password, but this works well with bcrypt. + * + * @ORM\Column(type="string", length=64) + */ + private $password; + + /** + * @ORM\Column(type="array") + */ + private $roles; + + public function __construct() { + $this->roles = array('ROLE_USER'); + } + + // other properties and methods + + public function getEmail() + { + return $this->email; + } + + public function setEmail($email) + { + $this->email = $email; + } + + public function getUsername() + { + return $this->username; + } + + public function setUsername($username) + { + $this->username = $username; + } + + public function getPlainPassword() + { + return $this->plainPassword; + } + + public function setPlainPassword($password) + { + $this->plainPassword = $password; + } + + public function getPassword() + { + return $this->password; + } + + public function setPassword($password) + { + $this->password = $password; + } + + public function getSalt() + { + // The bcrypt algorithm doesn't require a separate salt. + // You *may* need a real salt if you choose a different encoder. + return null; + } + + public function getRoles() + { + return $this->roles; + } + + public function eraseCredentials() + { + } + } + +The :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` requires +a few other methods and your ``security.yml`` file needs to be configured +properly to work with the ``User`` entity. For a more complete example, see +the :ref:`Entity Provider ` article. + +.. _registration-password-max: + +.. sidebar:: Why the 4096 Password Limit? + + Notice that the ``plainPassword`` field has a max length of 4096 characters. + For security purposes (`CVE-2013-5750`_), Symfony limits the plain password + length to 4096 characters when encoding it. Adding this constraint makes + sure that your form will give a validation error if anyone tries a super-long + password. + + You'll need to add this constraint anywhere in your application where + your user submits a plaintext password (e.g. change password form). The + only place where you don't need to worry about this is your login form, + since Symfony's Security component handles this for you. + +.. _create-a-form-for-the-model: + +Create a Form for the Entity +---------------------------- + +Next, create the form for the ``User`` entity:: + + // src/AppBundle/Form/UserType.php + namespace AppBundle\Form; + + use AppBundle\Entity\User; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class UserType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('email', 'email') + ->add('username', 'text') + ->add('plainPassword', 'repeated', array( + 'type' => 'password', + 'first_options' => array('label' => 'Password'), + 'second_options' => array('label' => 'Repeat Password'), + )) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => User::class, + )); + } + + public function getName() + { + return 'user'; + } + } + +There are just three fields: ``email``, ``username`` and ``plainPassword`` +(repeated to confirm the entered password). + +.. tip:: + + To explore more things about the Form component, read the + :doc:`/forms` guide. + +Handling the Form Submission +---------------------------- + +Next, you need a controller to handle the form rendering and submission. If the +form is submitted, the controller performs the validation and saves the data +into the database:: + + // src/AppBundle/Controller/RegistrationController.php + namespace AppBundle\Controller; + + use AppBundle\Form\UserType; + use AppBundle\Entity\User; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\HttpFoundation\Request; + + class RegistrationController extends Controller + { + /** + * @Route("/register", name="user_registration") + */ + public function registerAction(Request $request) + { + // 1) build the form + $user = new User(); + $form = $this->createForm(new UserType(), $user); + + // 2) handle the submit (will only happen on POST) + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + + // 3) Encode the password (you could also do this via Doctrine listener) + $password = $this->get('security.password_encoder') + ->encodePassword($user, $user->getPlainPassword()); + $user->setPassword($password); + + // 4) save the User! + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($user); + $entityManager->flush(); + + // ... do any other work - like sending them an email, etc + // maybe set a "flash" success message for the user + + return $this->redirectToRoute('replace_with_some_route'); + } + + return $this->render( + 'registration/register.html.twig', + array('form' => $form->createView()) + ); + } + } + +To define the algorithm used to encode the password in step 3 configure the +encoder in the security configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/security.yml + security: + encoders: + AppBundle\Entity\User: bcrypt + + .. code-block:: xml + + + + + + + bcrypt + + + + .. code-block:: php + + // app/config/security.php + use AppBundle\Entity\User; + + $container->loadFromExtension('security', array( + 'encoders' => array( + User::class => 'bcrypt', + ), + )); + +In this case the recommended ``bcrypt`` algorithm is used. If needed, check out +the :ref:`user password encoding ` article. + +.. note:: + + If you decide to NOT use annotation routing (shown above), then you'll + need to create a route to this controller: + + .. configuration-block:: + + .. code-block:: yaml + + # app/config/routing.yml + user_registration: + path: /register + defaults: { _controller: AppBundle:Registration:register } + + .. code-block:: xml + + + + + + + AppBundle:Registration:register + + + + .. code-block:: php + + // app/config/routing.php + use Symfony\Component\Routing\RouteCollection; + use Symfony\Component\Routing\Route; + + $routes = new RouteCollection(); + $routes->add('user_registration', new Route('/register', array( + '_controller' => 'AppBundle:Registration:register', + ))); + + return $routes; + +Next, create the template: + +.. configuration-block:: + + .. code-block:: html+twig + + {# app/Resources/views/registration/register.html.twig #} + + {{ form_start(form) }} + {{ form_row(form.username) }} + {{ form_row(form.email) }} + {{ form_row(form.plainPassword.first) }} + {{ form_row(form.plainPassword.second) }} + + + {{ form_end(form) }} + + .. code-block:: html+php + + + + start($form) ?> + row($form['username']) ?> + row($form['email']) ?> + + row($form['plainPassword']['first']) ?> + row($form['plainPassword']['second']) ?> + + + end($form) ?> + +See :doc:`/form/form_customization` for more details. + +Update your Database Schema +--------------------------- + +If you've updated the ``User`` entity during this tutorial, you have to update +your database schema using this command: + +.. code-block:: terminal + + $ php app/console doctrine:schema:update --force + +That's it! Head to ``/register`` to try things out! + +.. _registration-form-via-email: + +Having a Registration form with only Email (no Username) +-------------------------------------------------------- + +If you want your users to login via email and you don't need a username, then you +can remove it from your ``User`` entity entirely. Instead, make ``getUsername()`` +return the ``email`` property:: + + // src/AppBundle/Entity/User.php + // ... + + class User implements UserInterface + { + // ... + + public function getUsername() + { + return $this->email; + } + + // ... + } + +Next, just update the ``providers`` section of your ``security.yml`` file +so that Symfony knows how to load your users via the ``email`` property on +login. See :ref:`authenticating-someone-with-a-custom-entity-provider`. + +Adding a "accept terms" Checkbox +-------------------------------- + +Sometimes, you want a "Do you accept the terms and conditions" checkbox on your +registration form. The only trick is that you want to add this field to your form +without adding an unnecessary new ``termsAccepted`` property to your ``User`` entity +that you'll never need. + +To do this, add a ``termsAccepted`` field to your form, but set its +:ref:`mapped ` option to ``false``:: + + // src/AppBundle/Form/UserType.php + // ... + use Symfony\Component\Validator\Constraints\IsTrue; + + class UserType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('email', 'email'); + // ... + ->add('termsAccepted', 'checkbox', array( + 'mapped' => false, + 'constraints' => new IsTrue(), + )) + ); + } + } + +The :ref:`constraints ` option is also used, which allows +us to add validation, even though there is no ``termsAccepted`` property on ``User``. + +.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form +.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle diff --git a/doctrine/repository.rst b/doctrine/repository.rst new file mode 100644 index 00000000000..2c49a4dcdc2 --- /dev/null +++ b/doctrine/repository.rst @@ -0,0 +1,97 @@ +.. index:: + single: Doctrine; Custom Repository Class + +How to Create custom Repository Classes +======================================= + +In the previous sections, you began constructing and using more complex queries +from inside a controller. In order to isolate, reuse and test these queries, +it's a good practice to create a custom repository class for your entity. +Methods containing your query logic can then be stored in this class. + +To do this, add the repository class name to your entity's mapping definition: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/AppBundle/Entity/Product.php + namespace AppBundle\Entity; + + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository") + */ + class Product + { + //... + } + + .. code-block:: yaml + + # src/AppBundle/Resources/config/doctrine/Product.orm.yml + AppBundle\Entity\Product: + type: entity + repositoryClass: AppBundle\Repository\ProductRepository + # ... + + .. code-block:: xml + + + + + + + + + + + +Then, create an empty ``AppBundle\Repository\ProductRepository`` class extending +from ``Doctrine\ORM\EntityRepository``. + +Next, add a new method - ``findAllOrderedByName()`` - to the newly-generated +``ProductRepository`` class. This method will query for all the ``Product`` +entities, ordered alphabetically by name:: + + // src/AppBundle/Repository/ProductRepository.php + namespace AppBundle\Repository; + + use Doctrine\ORM\EntityRepository; + + class ProductRepository extends EntityRepository + { + public function findAllOrderedByName() + { + return $this->getEntityManager() + ->createQuery( + 'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC' + ) + ->getResult(); + } + } + +.. tip:: + + The entity manager can be accessed via ``$this->getEntityManager()`` + from inside the repository. + +You can use this new method just like the default finder methods of the repository:: + + use AppBundle\Entity\Post; + // ... + + $entityManager = $this->getDoctrine()->getManager(); + $products = $entityManager->getRepository(Product::class) + ->findAllOrderedByName(); + +.. note:: + + When using a custom repository class, you still have access to the default + finder methods such as ``find()`` and ``findAll()``. diff --git a/cookbook/doctrine/resolve_target_entity.rst b/doctrine/resolve_target_entity.rst similarity index 86% rename from cookbook/doctrine/resolve_target_entity.rst rename to doctrine/resolve_target_entity.rst index e5fc217a8e6..9c5901b354d 100644 --- a/cookbook/doctrine/resolve_target_entity.rst +++ b/doctrine/resolve_target_entity.rst @@ -21,8 +21,8 @@ without making them hard dependencies. Background ---------- -Suppose you have an `InvoiceBundle` which provides invoicing functionality -and a `CustomerBundle` that contains customer management tools. You want +Suppose you have an InvoiceBundle which provides invoicing functionality +and a CustomerBundle that contains customer management tools. You want to keep these separated, because they can be used in other systems without each other, but for your application you want to use them together. @@ -39,9 +39,9 @@ brevity) to explain how to set up and use the ``ResolveTargetEntityListener``. A Customer entity:: - // src/Acme/AppBundle/Entity/Customer.php + // src/AppBundle/Entity/Customer.php - namespace Acme\AppBundle\Entity; + namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Acme\CustomerBundle\Entity\Customer as BaseCustomer; @@ -118,21 +118,24 @@ about the replacement: orm: # ... resolve_target_entities: - Acme\InvoiceBundle\Model\InvoiceSubjectInterface: Acme\AppBundle\Entity\Customer + Acme\InvoiceBundle\Model\InvoiceSubjectInterface: AppBundle\Entity\Customer .. code-block:: xml + + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/doctrine + http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> - Acme\AppBundle\Entity\Customer + AppBundle\Entity\Customer @@ -140,11 +143,14 @@ about the replacement: .. code-block:: php // app/config/config.php + use Acme\InvoiceBundle\Model\InvoiceSubjectInterface; + use AppBundle\Entity\Customer; + $container->loadFromExtension('doctrine', array( 'orm' => array( // ... 'resolve_target_entities' => array( - 'Acme\InvoiceBundle\Model\InvoiceSubjectInterface' => 'Acme\AppBundle\Entity\Customer', + InvoiceSubjectInterface::class => Customer::class, ), ), )); diff --git a/cookbook/doctrine/reverse_engineering.rst b/doctrine/reverse_engineering.rst similarity index 93% rename from cookbook/doctrine/reverse_engineering.rst rename to doctrine/reverse_engineering.rst index dd50a6be9c2..ec48dc23455 100644 --- a/cookbook/doctrine/reverse_engineering.rst +++ b/doctrine/reverse_engineering.rst @@ -57,7 +57,7 @@ is to ask Doctrine to introspect the database and generate the corresponding metadata files. Metadata files describe the entity class to generate based on table fields. -.. code-block:: bash +.. code-block:: terminal $ php app/console doctrine:mapping:import --force AcmeBlogBundle xml @@ -88,21 +88,18 @@ The generated ``BlogPost.orm.xml`` metadata file looks as follows: Once the metadata files are generated, you can ask Doctrine to build related -entity classes by executing the following two commands. +entity classes by executing the following command. -.. code-block:: bash +.. code-block:: terminal + // generates entity classes with annotation mappings $ php app/console doctrine:mapping:convert annotation ./src - $ php app/console doctrine:generate:entities AcmeBlogBundle -The first command generates entity classes with annotation mappings. But -if you want to use YAML or XML mapping instead of annotations, you should -execute the second command only. +.. caution:: -.. tip:: - - If you want to use annotations, you can safely delete the XML (or YAML) files - after running these two commands. + If you want to use annotations, you must remove the XML (or YAML) files + after running this command. This is necessary as + :ref:`it is not possible to mix mapping configuration formats ` For example, the newly created ``BlogComment`` entity class looks as follow:: diff --git a/cookbook/email/email.rst b/email.rst similarity index 70% rename from cookbook/email/email.rst rename to email.rst index 6b205fb5770..06e537b77e3 100644 --- a/cookbook/email/email.rst +++ b/email.rst @@ -33,25 +33,29 @@ already included: # app/config/config.yml swiftmailer: - transport: "%mailer_transport%" - host: "%mailer_host%" - username: "%mailer_user%" - password: "%mailer_password%" + transport: '%mailer_transport%' + host: '%mailer_host%' + username: '%mailer_user%' + password: '%mailer_password%' .. code-block:: xml - - - - + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> + + +
.. code-block:: php @@ -69,19 +73,25 @@ can modify the values in that file, or set the values directly here. The following configuration attributes are available: -* ``transport`` (``smtp``, ``mail``, ``sendmail``, or ``gmail``) +* ``transport`` (``smtp``, ``mail``, ``sendmail``, or ``gmail``) * ``username`` * ``password`` * ``host`` * ``port`` -* ``encryption`` (``tls``, or ``ssl``) -* ``auth_mode`` (``plain``, ``login``, or ``cram-md5``) +* ``encryption`` (``tls``, or ``ssl``) +* ``auth_mode`` (``plain``, ``login``, or ``cram-md5``) * ``spool`` - * ``type`` (how to queue the messages, ``file`` or ``memory`` is supported, see :doc:`/cookbook/email/spool`) + * ``type`` (how to queue the messages, ``file`` or ``memory`` is supported, see :doc:`/email/spool`) * ``path`` (where to store the messages) -* ``delivery_address`` (an email address where to send ALL emails) -* ``disable_delivery`` (set to true to disable delivery completely) +* ``delivery_addresses`` (an array of email addresses where to send ALL emails) +* ``disable_delivery`` (set to true to disable delivery completely) + +.. caution:: + + Starting from SwiftMailer 5.4.5, the ``mail`` transport is deprecated + and will be removed in version 6. Consider using another transport like + ``smtp``, ``sendmail`` or ``gmail``. Sending Emails -------------- @@ -93,8 +103,7 @@ an email is pretty straightforward:: public function indexAction($name) { - $message = \Swift_Message::newInstance() - ->setSubject('Hello Email') + $message = (new \Swift_Message('Hello Email')) ->setFrom('send@example.com') ->setTo('recipient@example.com') ->setBody( @@ -116,6 +125,7 @@ an email is pretty straightforward:: ) */ ; + $this->get('mailer')->send($message); return $this->render(...); @@ -130,13 +140,15 @@ template might look something like this: {# app/Resources/views/Emails/registration.html.twig #}

You did it! You registered!

+ Hi {{ name }}! You're successfully registered. + {# example, assuming you have a route named "login" #} To login, go to: .... Thanks! {# Makes an absolute URL to the /images/logo.png file #} - .. versionadded:: 2.7 The ``absolute_url()`` function was introduced in Symfony 2.7. Prior @@ -147,17 +159,17 @@ The ``$message`` object supports many more options, such as including attachment adding HTML content, and much more. Fortunately, Swift Mailer covers the topic of `Creating Messages`_ in great detail in its documentation. -.. tip:: +Learn more +---------- - Several other cookbook articles are available related to sending emails - in Symfony: +.. toctree:: + :maxdepth: 1 + :glob: - * :doc:`gmail` - * :doc:`dev_environment` - * :doc:`spool` + email/* .. _`Swift Mailer`: http://swiftmailer.org/ -.. _`Creating Messages`: http://swiftmailer.org/docs/messages.html +.. _`Creating Messages`: https://swiftmailer.symfony.com/docs/messages.html .. _`Mandrill`: https://mandrill.com/ .. _`SendGrid`: https://sendgrid.com/ .. _`Amazon SES`: http://aws.amazon.com/ses/ diff --git a/cookbook/email/cloud.rst b/email/cloud.rst similarity index 82% rename from cookbook/email/cloud.rst rename to email/cloud.rst index 9b3677094c3..d7ddbd33f3b 100644 --- a/cookbook/email/cloud.rst +++ b/email/cloud.rst @@ -7,12 +7,12 @@ How to Use the Cloud to Send Emails Requirements for sending emails from a production system differ from your development setup as you don't want to be limited in the number of emails, the sending rate or the sender address. Thus, -:doc:`using Gmail ` or similar services is not an +:doc:`using Gmail ` or similar services is not an option. If setting up and maintaining your own reliable mail server causes you a headache there's a simple solution: Leverage the cloud to send your emails. -This cookbook shows how easy it is to integrate +This article shows how easy it is to integrate `Amazon's Simple Email Service (SES)`_ into Symfony. .. note:: @@ -34,10 +34,10 @@ and complete the configuration with the provided ``username`` and ``password``: swiftmailer: transport: smtp host: email-smtp.us-east-1.amazonaws.com - port: 465 # different ports are available, see SES console + port: 587 # different ports are available, see SES console encryption: tls # TLS encryption is required - username: AWS_ACCESS_KEY # to be created in the SES console - password: AWS_SECRET_KEY # to be created in the SES console + username: AWS_SES_SMTP_USERNAME # to be created in the SES console + password: AWS_SES_SMTP_PASSWORD # to be created in the SES console .. code-block:: xml @@ -55,10 +55,10 @@ and complete the configuration with the provided ``username`` and ``password``:
@@ -68,10 +68,10 @@ and complete the configuration with the provided ``username`` and ``password``: $container->loadFromExtension('swiftmailer', array( 'transport' => 'smtp', 'host' => 'email-smtp.us-east-1.amazonaws.com', - 'port' => 465, + 'port' => 587, 'encryption' => 'tls', - 'username' => 'AWS_ACCESS_KEY', - 'password' => 'AWS_SECRET_KEY', + 'username' => 'AWS_SES_SMTP_USERNAME', + 'password' => 'AWS_SES_SMTP_PASSWORD', )); The ``port`` and ``encryption`` keys are not present in the Symfony Standard @@ -94,10 +94,10 @@ And that's it, you're ready to start sending emails through the cloud! # ... mailer_transport: smtp mailer_host: email-smtp.us-east-1.amazonaws.com - mailer_port: 465 # different ports are available, see SES console + mailer_port: 587 # different ports are available, see SES console mailer_encryption: tls # TLS encryption is required - mailer_user: AWS_ACCESS_KEY # to be created in the SES console - mailer_password: AWS_SECRET_KEY # to be created in the SES console + mailer_user: AWS_SES_SMTP_USERNAME # to be created in the SES console + mailer_password: AWS_SES_SMTP_PASSWORD # to be created in the SES console .. note:: diff --git a/cookbook/email/dev_environment.rst b/email/dev_environment.rst similarity index 58% rename from cookbook/email/dev_environment.rst rename to email/dev_environment.rst index d5dbabba918..739c9fe9f8b 100644 --- a/cookbook/email/dev_environment.rst +++ b/email/dev_environment.rst @@ -28,36 +28,40 @@ will not be sent when you run tests, but will continue to be sent in the # app/config/config_test.yml swiftmailer: - disable_delivery: true + disable_delivery: true .. code-block:: xml - - + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - + + .. code-block:: php // app/config/config_test.php $container->loadFromExtension('swiftmailer', array( - 'disable_delivery' => "true", + 'disable_delivery' => "true", )); If you'd also like to disable deliver in the ``dev`` environment, simply add this same configuration to the ``config_dev.yml`` file. -Sending to a Specified Address ------------------------------- +.. _sending-to-a-specified-address: -You can also choose to have all email sent to a specific address, instead +Sending to a Specified Address(es) +---------------------------------- + +You can also choose to have all email sent to a specific address or a list of addresses, instead of the address actually specified when sending the message. This can be done -via the ``delivery_address`` option: +via the ``delivery_addresses`` option: .. configuration-block:: @@ -65,34 +69,37 @@ via the ``delivery_address`` option: # app/config/config_dev.yml swiftmailer: - delivery_address: dev@example.com + delivery_addresses: ['dev@example.com'] .. code-block:: xml - - + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/swiftmailer + http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - + + dev@example.com + + .. code-block:: php // app/config/config_dev.php $container->loadFromExtension('swiftmailer', array( - 'delivery_address' => "dev@example.com", + 'delivery_addresses' => array("dev@example.com"), )); -Now, suppose you're sending an email to ``recipient@example.com``. - -.. code-block:: php +Now, suppose you're sending an email to ``recipient@example.com``:: public function indexAction($name) { - $message = \Swift_Message::newInstance() - ->setSubject('Hello Email') + $message = (new \Swift_Message('Hello Email')) ->setFrom('send@example.com') ->setTo('recipient@example.com') ->setBody( @@ -102,6 +109,7 @@ Now, suppose you're sending an email to ``recipient@example.com``. ) ) ; + $this->get('mailer')->send($message); return $this->render(...); @@ -136,51 +144,54 @@ by adding the ``delivery_whitelist`` option: # app/config/config_dev.yml swiftmailer: - delivery_address: dev@example.com + delivery_addresses: ['dev@example.com'] delivery_whitelist: - # all email addresses matching this regex will *not* be - # redirected to dev@example.com - - "/@specialdomain.com$/" - - # all emails sent to admin@mydomain.com won't - # be redirected to dev@example.com too - - "/^admin@mydomain.com$/" + # all email addresses matching these regexes will be delivered + # like normal, as well as being sent to dev@example.com + - '/@specialdomain\.com$/' + - '/^admin@mydomain\.com$/' .. code-block:: xml - - + - - - - /@specialdomain.com$/ - - - /^admin@mydomain.com$/ - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer" + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/swiftmailer + http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> + + + + /@specialdomain\.com$/ + /^admin@mydomain\.com$/ + dev@example.com + + .. code-block:: php // app/config/config_dev.php $container->loadFromExtension('swiftmailer', array( - 'delivery_address' => "dev@example.com", + 'delivery_addresses' => array("dev@example.com"), 'delivery_whitelist' => array( - // all email addresses matching this regex will *not* be - // redirected to dev@example.com - '/@specialdomain.com$/', - - // all emails sent to admin@mydomain.com won't be - // redirected to dev@example.com too - '/^admin@mydomain.com$/', + // all email addresses matching these regexes will be delivered + // like normal, as well as being sent to dev@example.com + '/@specialdomain\.com$/', + '/^admin@mydomain\.com$/', ), )); -In the above example all email messages will be redirected to ``dev@example.com``, -except messages sent to the ``admin@mydomain.com`` address or to any email -address belonging to the domain ``specialdomain.com``, which will be delivered as normal. +In the above example all email messages will be redirected to ``dev@example.com`` +and messages sent to the ``admin@mydomain.com`` address or to any email address +belonging to the domain ``specialdomain.com`` will also be delivered as normal. + +.. caution:: + + The ``delivery_whitelist`` option is ignored unless the ``delivery_addresses`` option is defined. Viewing from the Web Debug Toolbar ---------------------------------- @@ -209,16 +220,19 @@ you to open the report with details of the sent emails. .. code-block:: xml - - - - + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/webprofiler + http://symfony.com/schema/dic/webprofiler/webprofiler-1.0.xsd"> + + + .. code-block:: php diff --git a/email/gmail.rst b/email/gmail.rst new file mode 100644 index 00000000000..e34604bae0a --- /dev/null +++ b/email/gmail.rst @@ -0,0 +1,133 @@ +.. index:: + single: Emails; Gmail + +How to Use Gmail to Send Emails +=============================== + +During development, instead of using a regular SMTP server to send emails, you +might find using Gmail easier and more practical. The SwiftmailerBundle makes +it really easy. + +In the development configuration file, change the ``transport`` setting to +``gmail`` and set the ``username`` and ``password`` to the Google credentials: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config_dev.yml + swiftmailer: + transport: gmail + username: your_gmail_username + password: your_gmail_password + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // app/config/config_dev.php + $container->loadFromExtension('swiftmailer', array( + 'transport' => 'gmail', + 'username' => 'your_gmail_username', + 'password' => 'your_gmail_password', + )); + +.. tip:: + + It's more convenient to configure these options in the ``parameters.yml`` + file: + + .. code-block:: yaml + + # app/config/parameters.yml + parameters: + # ... + mailer_user: your_gmail_username + mailer_password: your_gmail_password + + .. configuration-block:: + + .. code-block:: yaml + + # app/config/config_dev.yml + swiftmailer: + transport: gmail + username: '%mailer_user%' + password: '%mailer_password%' + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // app/config/config_dev.php + $container->loadFromExtension('swiftmailer', array( + 'transport' => 'gmail', + 'username' => '%mailer_user%', + 'password' => '%mailer_password%', + )); + +Redefining the Default Configuration Parameters +----------------------------------------------- + +The ``gmail`` transport is simply a shortcut that uses the ``smtp`` transport +and sets these options: + +============== ================== +Option Value +============== ================== +``encryption`` ``ssl`` +``auth_mode`` ``login`` +``host`` ``smtp.gmail.com`` +============== ================== + +If your application uses ``tls`` encryption or ``oauth`` authentication, you +must override the default options by defining the ``encryption`` and ``auth_mode`` +parameters. + +If your Gmail account uses 2-Step-Verification, you must `generate an App password`_ +and use it as the value of the ``mailer_password`` parameter. You must also ensure +that you `allow less secure apps to access your Gmail account`_. + +.. seealso:: + + See the :doc:`Swiftmailer configuration reference ` + for more details. + +.. _`generate an App password`: https://support.google.com/accounts/answer/185833 +.. _`allow less secure apps to access your Gmail account`: https://support.google.com/accounts/answer/6010255 diff --git a/cookbook/email/spool.rst b/email/spool.rst similarity index 52% rename from cookbook/email/spool.rst rename to email/spool.rst index db40c23ba44..53622386f49 100644 --- a/cookbook/email/spool.rst +++ b/email/spool.rst @@ -35,29 +35,37 @@ swiftmailer with the memory option, use the following configuration: .. code-block:: xml - - + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> - - - + + + + .. code-block:: php // app/config/config.php $container->loadFromExtension('swiftmailer', array( // ... - 'spool' => array('type' => 'memory') + 'spool' => array('type' => 'memory'), )); -Spool Using a File +.. _spool-using-a-file: + +Spool Using Files ------------------ -In order to use the spool with a file, use the following configuration: +When you use the filesystem for spooling, Symfony creates a folder in the given +path for each mail service (e.g. "default" for the default service). This folder +will contain files for each email in the spool. So make sure this directory is +writable by Symfony (or your webserver/php)! + +In order to use the spool with files, use the following configuration: .. configuration-block:: @@ -68,23 +76,26 @@ In order to use the spool with a file, use the following configuration: # ... spool: type: file - path: /path/to/spool + path: /path/to/spooldir .. code-block:: xml - - - - - - + xsi:schemaLocation="http://symfony.com/schema/dic/services + http://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd"> + + + + + .. code-block:: php @@ -94,7 +105,7 @@ In order to use the spool with a file, use the following configuration: 'spool' => array( 'type' => 'file', - 'path' => '/path/to/spool', + 'path' => '/path/to/spooldir', ), )); @@ -106,28 +117,46 @@ In order to use the spool with a file, use the following configuration: .. code-block:: yaml - path: "%kernel.root_dir%/spool" + path: '%kernel.root_dir%/spool' Now, when your app sends an email, it will not actually be sent but instead added to the spool. Sending the messages from the spool is done separately. There is a console command to send the messages in the spool: -.. code-block:: bash +.. code-block:: terminal $ php app/console swiftmailer:spool:send --env=prod It has an option to limit the number of messages to be sent: -.. code-block:: bash +.. code-block:: terminal $ php app/console swiftmailer:spool:send --message-limit=10 --env=prod You can also set the time limit in seconds: -.. code-block:: bash +.. code-block:: terminal $ php app/console swiftmailer:spool:send --time-limit=10 --env=prod Of course you will not want to run this manually in reality. Instead, the console command should be triggered by a cron job or scheduled task and run at a regular interval. + +.. caution:: + + When you create a message with SwiftMailer, it generates a ``Swift_Message`` + class. If the ``swiftmailer`` service is lazy loaded, it generates instead a + proxy class named ``Swift_Message_``. + + If you use the memory spool, this change is transparent and has no impact. + But when using the filesystem spool, the message class is serialized in + a file with the randomized class name. The problem is that this random + class name changes on every cache clear. So if you send a mail and then you + clear the cache, the message will not be unserializable. + + On the next execution of ``swiftmailer:spool:send`` an error will raise because + the class ``Swift_Message_`` doesn't exist (anymore). + + The solutions are either to use the memory spool or to load the + ``swiftmailer`` service without the ``lazy`` option (see :doc:`/service_container/lazy_services`). diff --git a/cookbook/email/testing.rst b/email/testing.rst similarity index 56% rename from cookbook/email/testing.rst rename to email/testing.rst index 3797f81b582..95df725142c 100644 --- a/cookbook/email/testing.rst +++ b/email/testing.rst @@ -8,14 +8,13 @@ Sending emails with Symfony is pretty straightforward thanks to the SwiftmailerBundle, which leverages the power of the `Swift Mailer`_ library. To functionally test that an email was sent, and even assert the email subject, -content or any other headers, you can use :doc:`the Symfony Profiler `. +content or any other headers, you can use :doc:`the Symfony Profiler `. Start with an easy controller action that sends an email:: public function sendEmailAction($name) { - $message = \Swift_Message::newInstance() - ->setSubject('Hello Email') + $message = (new \Swift_Message('Hello Email')) ->setFrom('send@example.com') ->setTo('recipient@example.com') ->setBody('You should see me from the profiler!') @@ -26,12 +25,8 @@ Start with an easy controller action that sends an email:: return $this->render(...); } -.. note:: - - Don't forget to enable the profiler as explained in :doc:`/cookbook/testing/profiling`. - In your functional test, use the ``swiftmailer`` collector on the profiler -to get information about the messages send on the previous request:: +to get information about the messages sent on the previous request:: // src/AppBundle/Tests/Controller/MailControllerTest.php use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; @@ -42,29 +37,47 @@ to get information about the messages send on the previous request:: { $client = static::createClient(); - // Enable the profiler for the next request (it does nothing if the profiler is not available) + // enables the profiler for the next request (it does nothing if the profiler is not available) $client->enableProfiler(); $crawler = $client->request('POST', '/path/to/above/action'); $mailCollector = $client->getProfile()->getCollector('swiftmailer'); - // Check that an email was sent - $this->assertEquals(1, $mailCollector->getMessageCount()); + // checks that an email was sent + $this->assertSame(1, $mailCollector->getMessageCount()); $collectedMessages = $mailCollector->getMessages(); $message = $collectedMessages[0]; // Asserting email data $this->assertInstanceOf('Swift_Message', $message); - $this->assertEquals('Hello Email', $message->getSubject()); - $this->assertEquals('send@example.com', key($message->getFrom())); - $this->assertEquals('recipient@example.com', key($message->getTo())); - $this->assertEquals( + $this->assertSame('Hello Email', $message->getSubject()); + $this->assertSame('send@example.com', key($message->getFrom())); + $this->assertSame('recipient@example.com', key($message->getTo())); + $this->assertSame( 'You should see me from the profiler!', $message->getBody() ); } } +Troubleshooting +--------------- + +Problem: The Collector Object Is ``null`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The email collector is only available when the profiler is enabled and collects +information, as explained in :doc:`/testing/profiling`. + +Problem: The Collector Doesn't Contain the E-Mail +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a redirection is performed after sending the email (for example when you send +an email after a form is processed and before redirecting to another page), make +sure that the test client doesn't follow the redirects, as explained in +:doc:`/testing`. Otherwise, the collector will contain the information of the +redirected page and the email won't be accessible. + .. _`Swift Mailer`: http://swiftmailer.org/ diff --git a/event_dispatcher.rst b/event_dispatcher.rst new file mode 100644 index 00000000000..4d476649c47 --- /dev/null +++ b/event_dispatcher.rst @@ -0,0 +1,295 @@ +.. index:: + single: Events; Create listener + single: Create subscriber + +Events and Event Listeners +========================== + +During the execution of a Symfony application, lots of event notifications are +triggered. Your application can listen to these notifications and respond to +them by executing any piece of code. + +Symfony triggers several :doc:`events related to the kernel ` +while processing the HTTP Request. Third-party bundles may also dispatch events, and +you can even dispatch :doc:`custom events ` from your +own code. + +All the examples shown in this article use the same ``KernelEvents::EXCEPTION`` +event for consistency purposes. In your own application, you can use any event +and even mix several of them in the same subscriber. + +Creating an Event Listener +-------------------------- + +The most common way to listen to an event is to register an **event listener**:: + + // src/AppBundle/EventListener/ExceptionListener.php + namespace AppBundle\EventListener; + + use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; + + class ExceptionListener + { + public function onKernelException(GetResponseForExceptionEvent $event) + { + // You get the exception object from the received event + $exception = $event->getException(); + $message = sprintf( + 'My Error says: %s with code: %s', + $exception->getMessage(), + $exception->getCode() + ); + + // Customize your response object to display the exception details + $response = new Response(); + $response->setContent($message); + + // HttpExceptionInterface is a special type of exception that + // holds status code and header details + if ($exception instanceof HttpExceptionInterface) { + $response->setStatusCode($exception->getStatusCode()); + $response->headers->replace($exception->getHeaders()); + } else { + $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); + } + + // sends the modified response object to the event + $event->setResponse($response); + } + } + +.. tip:: + + Each event receives a slightly different type of ``$event`` object. For + the ``kernel.exception`` event, it is :class:`Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent`. + Check out the :doc:`Symfony events reference ` to see + what type of object each event provides. + +Now that the class is created, you just need to register it as a service and +notify Symfony that it is a "listener" on the ``kernel.exception`` event by +using a special "tag": + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + app.exception_listener: + class: AppBundle\EventListener\ExceptionListener + tags: + - { name: kernel.event_listener, event: kernel.exception } + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\EventListener\ExceptionListener; + + $container + ->register('app.exception_listener', ExceptionListener::class) + ->addTag('kernel.event_listener', array('event' => 'kernel.exception')) + ; + +.. note:: + + There is an optional tag attribute called ``method`` which defines which method + to execute when the event is triggered. By default the name of the method is + ``on`` + "camel-cased event name". If the event is ``kernel.exception`` the + method executed by default is ``onKernelException()``. + + The other optional tag attribute is called ``priority``, which defaults to + ``0`` and it controls the order in which listeners are executed (the higher + the priority the earlier a listener is executed). This is useful when you + need to guarantee that one listener is executed before another. The priorities + of the internal Symfony listeners usually range from ``-255`` to ``255`` but + your own listeners can use any positive or negative integer. + +Creating an Event Subscriber +---------------------------- + +Another way to listen to events is via an **event subscriber**, which is a class +that defines one or more methods that listen to one or various events. The main +difference with the event listeners is that subscribers always know which events +they are listening to. + +In a given subscriber, different methods can listen to the same event. The order +in which methods are executed is defined by the ``priority`` parameter of each +method (the higher the priority the earlier the method is called). To learn more +about event subscribers, read :doc:`/components/event_dispatcher`. + +The following example shows an event subscriber that defines several methods which +listen to the same ``kernel.exception`` event:: + + // src/AppBundle/EventSubscriber/ExceptionSubscriber.php + namespace AppBundle\EventSubscriber; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; + use Symfony\Component\HttpKernel\KernelEvents; + + class ExceptionSubscriber implements EventSubscriberInterface + { + public static function getSubscribedEvents() + { + // return the subscribed events, their methods and priorities + return array( + KernelEvents::EXCEPTION => array( + array('processException', 10), + array('logException', 0), + array('notifyException', -10), + ) + ); + } + + public function processException(GetResponseForExceptionEvent $event) + { + // ... + } + + public function logException(GetResponseForExceptionEvent $event) + { + // ... + } + + public function notifyException(GetResponseForExceptionEvent $event) + { + // ... + } + } + +Now, you just need to register the class as a service and add the +``kernel.event_subscriber`` tag to tell Symfony that this is an event subscriber: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + app.exception_subscriber: + class: AppBundle\EventSubscriber\ExceptionSubscriber + tags: + - { name: kernel.event_subscriber } + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\EventSubscriber\ExceptionSubscriber; + + $container + ->register('app.exception_subscriber', ExceptionSubscriber::class) + ->addTag('kernel.event_subscriber') + ; + +Request Events, Checking Types +------------------------------ + +A single page can make several requests (one master request, and then multiple +sub-requests - typically by :doc:`/templating/embedding_controllers`). For the core +Symfony events, you might need to check to see if the event is for a "master" request +or a "sub request":: + + // src/AppBundle/EventListener/RequestListener.php + namespace AppBundle\EventListener; + + use Symfony\Component\HttpKernel\Event\GetResponseEvent; + use Symfony\Component\HttpKernel\HttpKernel; + use Symfony\Component\HttpKernel\HttpKernelInterface; + + class RequestListener + { + public function onKernelRequest(GetResponseEvent $event) + { + if (!$event->isMasterRequest()) { + // don't do anything if it's not the master request + return; + } + + // ... + } + } + +Certain things, like checking information on the *real* request, may not need to +be done on the sub-request listeners. + +.. _events-or-subscribers: + +Listeners or Subscribers +------------------------ + +Listeners and subscribers can be used in the same application indistinctly. The +decision to use either of them is usually a matter of personal taste. However, +there are some minor advantages for each of them: + +* **Subscribers are easier to reuse** because the knowledge of the events is kept + in the class rather than in the service definition. This is the reason why + Symfony uses subscribers internally; +* **Listeners are more flexible** because bundles can enable or disable each of + them conditionally depending on some configuration value. + +Debugging Event Listeners +------------------------- + +.. versionadded:: 2.6 + The ``debug:event-dispatcher`` command was introduced in Symfony 2.6. + +You can find out what listeners are registered in the event dispatcher +using the console. To show all events and their listeners, run: + +.. code-block:: terminal + + $ php app/console debug:event-dispatcher + +You can get registered listeners for a particular event by specifying +its name: + +.. code-block:: terminal + + $ php app/console debug:event-dispatcher kernel.exception + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + event_dispatcher/* diff --git a/cookbook/event_dispatcher/before_after_filters.rst b/event_dispatcher/before_after_filters.rst similarity index 69% rename from cookbook/event_dispatcher/before_after_filters.rst rename to event_dispatcher/before_after_filters.rst index d1c5acddbf8..0ed6588c0e6 100644 --- a/cookbook/event_dispatcher/before_after_filters.rst +++ b/event_dispatcher/before_after_filters.rst @@ -1,8 +1,8 @@ .. index:: single: EventDispatcher -How to Setup before and after Filters -===================================== +How to Set Up Before and After Filters +====================================== It is quite common in web application development to need some logic to be executed just before or just after your controller actions acting as filters @@ -11,7 +11,7 @@ or hooks. In symfony1, this was achieved with the preExecute and postExecute methods. Most major frameworks have similar methods but there is no such thing in Symfony. The good news is that there is a much better way to interfere with the -Request -> Response process using the :doc:`EventDispatcher component `. +Request -> Response process using the :doc:`EventDispatcher component `. Token Validation Example ------------------------ @@ -49,12 +49,19 @@ parameters key: .. code-block:: xml - - - pass1 - pass2 - - + + + + + + pass1 + pass2 + + + .. code-block:: php @@ -101,8 +108,8 @@ Creating an Event Listener ~~~~~~~~~~~~~~~~~~~~~~~~~~ Next, you'll need to create an event listener, which will hold the logic -that you want executed before your controllers. If you're not familiar with -event listeners, you can learn more about them at :doc:`/cookbook/service_container/event_listener`:: +that you want to be executed before your controllers. If you're not familiar with +event listeners, you can learn more about them at :doc:`/event_dispatcher`:: // src/AppBundle/EventListener/TokenListener.php namespace AppBundle\EventListener; @@ -157,31 +164,40 @@ your listener to be called just before any controller is executed. services: app.tokens.action_listener: class: AppBundle\EventListener\TokenListener - arguments: ["%tokens%"] + arguments: ['%tokens%'] tags: - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } .. code-block:: xml - - %tokens% - - + + + + + + %tokens% + + + + .. code-block:: php // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; + use AppBundle\EventListener\TokenListener; - $listener = new Definition('AppBundle\EventListener\TokenListener', array('%tokens%')); - $listener->addTag('kernel.event_listener', array( - 'event' => 'kernel.controller', - 'method' => 'onKernelController' - )); - $container->setDefinition('app.tokens.action_listener', $listener); + $container->register('app.tokens.action_listener', TokenListener::class) + ->addArgument('%tokens%') + ->addTag('kernel.event_listener', array( + 'event' => 'kernel.controller', + 'method' => 'onKernelController', + )); -With this configuration, your ``TokenListener`` ``onKernelController`` method +With this configuration, your ``TokenListener`` ``onKernelController()`` method will be executed on each request. If the controller that is about to be executed implements ``TokenAuthenticatedController``, token authentication is applied. This lets you have a "before" filter on any controller that you @@ -219,7 +235,7 @@ serve as a basic flag that this request underwent token authentication:: } } -Now, add another method to this class - ``onKernelResponse`` - that looks +Now, add another method to this class - ``onKernelResponse()`` - that looks for this flag on the request object and sets a custom header on the response if it's found:: @@ -252,7 +268,7 @@ event: services: app.tokens.action_listener: class: AppBundle\EventListener\TokenListener - arguments: ["%tokens%"] + arguments: ['%tokens%'] tags: - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } @@ -260,31 +276,40 @@ event: .. code-block:: xml - - %tokens% - - - + + + + + + %tokens% + + + + + .. code-block:: php // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; - - $listener = new Definition('AppBundle\EventListener\TokenListener', array('%tokens%')); - $listener->addTag('kernel.event_listener', array( - 'event' => 'kernel.controller', - 'method' => 'onKernelController' - )); - $listener->addTag('kernel.event_listener', array( - 'event' => 'kernel.response', - 'method' => 'onKernelResponse' - )); - $container->setDefinition('app.tokens.action_listener', $listener); + use AppBundle\EventListener\TokenListener; + + $container->register('app.tokens.action_listener', TokenListener::class) + ->addArgument('%tokens%') + ->addTag('kernel.event_listener', array( + 'event' => 'kernel.controller', + 'method' => 'onKernelController', + )) + ->addTag('kernel.event_listener', array( + 'event' => 'kernel.response', + 'method' => 'onKernelResponse', + )); That's it! The ``TokenListener`` is now notified before every controller is -executed (``onKernelController``) and after every controller returns a response -(``onKernelResponse``). By making specific controllers implement the ``TokenAuthenticatedController`` +executed (``onKernelController()``) and after every controller returns a response +(``onKernelResponse()``). By making specific controllers implement the ``TokenAuthenticatedController`` interface, your listener knows which controllers it should take action on. -And by storing a value in the request's "attributes" bag, the ``onKernelResponse`` +And by storing a value in the request's "attributes" bag, the ``onKernelResponse()`` method knows to add the extra header. Have fun! diff --git a/cookbook/event_dispatcher/class_extension.rst b/event_dispatcher/class_extension.rst similarity index 88% rename from cookbook/event_dispatcher/class_extension.rst rename to event_dispatcher/class_extension.rst index d6b961fb17d..776ee1b92e1 100644 --- a/cookbook/event_dispatcher/class_extension.rst +++ b/event_dispatcher/class_extension.rst @@ -5,9 +5,7 @@ How to Extend a Class without Using Inheritance =============================================== To allow multiple classes to add methods to another one, you can define the -magic ``__call()`` method in the class you want to be extended like this: - -.. code-block:: php +magic ``__call()`` method in the class you want to be extended like this:: class Foo { @@ -15,7 +13,7 @@ magic ``__call()`` method in the class you want to be extended like this: public function __call($method, $arguments) { - // create an event named 'foo.method_is_not_found' + // creates an event named 'foo.method_is_not_found' $event = new HandleUndefinedMethodEvent($this, $method, $arguments); $this->dispatcher->dispatch('foo.method_is_not_found', $event); @@ -24,16 +22,14 @@ magic ``__call()`` method in the class you want to be extended like this: throw new \Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method)); } - // return the listener returned value + // returns the listener returned value return $event->getReturnValue(); } } This uses a special ``HandleUndefinedMethodEvent`` that should also be created. This is a generic class that could be reused each time you need to -use this pattern of class extension: - -.. code-block:: php +use this pattern of class extension:: use Symfony\Component\EventDispatcher\Event; @@ -89,9 +85,7 @@ use this pattern of class extension: } Next, create a class that will listen to the ``foo.method_is_not_found`` event -and *add* the method ``bar()``: - -.. code-block:: php +and *add* the method ``bar()``:: class Bar { @@ -116,10 +110,8 @@ and *add* the method ``bar()``: } } -Finally, add the new ``bar`` method to the ``Foo`` class by registering an -instance of ``Bar`` with the ``foo.method_is_not_found`` event: - -.. code-block:: php +Finally, add the new ``bar()`` method to the ``Foo`` class by registering an +instance of ``Bar`` with the ``foo.method_is_not_found`` event:: $bar = new Bar(); $dispatcher->addListener('foo.method_is_not_found', array($bar, 'onFooMethodIsNotFound')); diff --git a/cookbook/event_dispatcher/method_behavior.rst b/event_dispatcher/method_behavior.rst similarity index 79% rename from cookbook/event_dispatcher/method_behavior.rst rename to event_dispatcher/method_behavior.rst index 5ad5da29441..0356899ebc3 100644 --- a/cookbook/event_dispatcher/method_behavior.rst +++ b/event_dispatcher/method_behavior.rst @@ -26,10 +26,10 @@ method:: $bar = $event->getBar(); // the real method implementation is here - $ret = ...; + $returnValue = ...; // do something after the method - $event = new FilterSendReturnValue($ret); + $event = new FilterSendReturnValue($returnValue); $this->dispatcher->dispatch('foo.post_send', $event); return $event->getReturnValue(); @@ -40,18 +40,16 @@ In this example, two events are thrown: ``foo.pre_send``, before the method is executed, and ``foo.post_send`` after the method is executed. Each uses a custom Event class to communicate information to the listeners of the two events. These event classes would need to be created by you and should allow, -in this example, the variables ``$foo``, ``$bar`` and ``$ret`` to be retrieved +in this example, the variables ``$foo``, ``$bar`` and ``$returnValue`` to be retrieved and set by the listeners. -For example, assuming the ``FilterSendReturnValue`` has a ``setReturnValue`` -method, one listener might look like this: - -.. code-block:: php +For example, assuming the ``FilterSendReturnValue`` has a ``setReturnValue()`` +method, one listener might look like this:: public function onFooPostSend(FilterSendReturnValue $event) { - $ret = $event->getReturnValue(); - // modify the original ``$ret`` value + $returnValue = $event->getReturnValue(); + // modify the original ``$returnValue`` value - $event->setReturnValue($ret); + $event->setReturnValue($returnValue); } diff --git a/form/action_method.rst b/form/action_method.rst new file mode 100644 index 00000000000..b3f5e97a303 --- /dev/null +++ b/form/action_method.rst @@ -0,0 +1,133 @@ +.. index:: + single: Forms; Changing the action and method + +How to Change the Action and Method of a Form +============================================= + +By default, a form will be submitted via an HTTP POST request to the same +URL under which the form was rendered. Sometimes you want to change these +parameters. You can do so in a few different ways. + +If you use the :class:`Symfony\\Component\\Form\\FormBuilder` to build your +form, you can use ``setAction()`` and ``setMethod()``: + +.. configuration-block:: + + .. code-block:: php-symfony + + // AppBundle/Controller/DefaultController.php + namespace AppBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + + class DefaultController extends Controller + { + public function newAction() + { + $form = $this->createFormBuilder($task) + ->setAction($this->generateUrl('target_route')) + ->setMethod('GET') + ->add('task', 'text') + ->add('dueDate', 'date') + ->add('save', 'submit') + ->getForm(); + + // ... + } + } + + .. code-block:: php-standalone + + use Symfony\Component\Form\Forms; + + // ... + + $formFactoryBuilder = Forms::createFormFactoryBuilder(); + + // Form factory builder configuration ... + + $formFactory = $formFactoryBuilder->getFormFactory(); + + $form = $formFactory->createBuilder('form', $task) + ->setAction('...') + ->setMethod('GET') + ->add('task', 'text') + ->add('dueDate', 'date') + ->add('save', 'submit') + ->getForm(); + +.. note:: + + This example assumes that you've created a route called ``target_route`` + that points to the controller that processes the form. + +When using a form type class, you can pass the action and method as form +options: + +.. configuration-block:: + + .. code-block:: php-symfony + + // AppBundle/Controller/DefaultController.php + namespace AppBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use AppBundle\Form\TaskType; + + class DefaultController extends Controller + { + public function newAction() + { + // ... + + $form = $this->createForm(new TaskType(), $task, array( + 'action' => $this->generateUrl('target_route'), + 'method' => 'GET', + )); + + // ... + } + } + + .. code-block:: php-standalone + + use Symfony\Component\Form\Forms; + use AppBundle\Form\TaskType; + + $formFactoryBuilder = Forms::createFormFactoryBuilder(); + + // Form factory builder configuration ... + + $formFactory = $formFactoryBuilder->getFormFactory(); + + $form = $formFactory->create(new TaskType(), $task, array( + 'action' => '...', + 'method' => 'GET', + )); + +Finally, you can override the action and method in the template by passing them +to the ``form()`` or the ``form_start()`` helper functions: + +.. configuration-block:: + + .. code-block:: html+twig + + {# app/Resources/views/default/new.html.twig #} + {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }} + + .. code-block:: html+php + + + start($form, array( + 'action' => $view['router']->generate('target_route'), + 'method' => 'GET', + )) ?> + +.. note:: + + If the form's method is not GET or POST, but PUT, PATCH or DELETE, Symfony + will insert a hidden field with the name ``_method`` that stores this method. + The form will be submitted in a normal POST request, but Symfony's router + is capable of detecting the ``_method`` parameter and will interpret it as + a PUT, PATCH or DELETE request. See the :ref:`configuration-framework-http_method_override` + option. diff --git a/form/button_based_validation.rst b/form/button_based_validation.rst new file mode 100644 index 00000000000..7e2c21bf1b1 --- /dev/null +++ b/form/button_based_validation.rst @@ -0,0 +1,42 @@ +.. index:: + single: Forms; Validation groups based on clicked button + +How to Choose Validation Groups Based on the Clicked Button +=========================================================== + +.. versionadded:: 2.3 + Support for buttons in forms was introduced in Symfony 2.3. + +When your form contains multiple submit buttons, you can change the validation +group depending on which button is used to submit the form. For example, +consider a form in a wizard that lets you advance to the next step or go back +to the previous step. Also assume that when returning to the previous step, +the data of the form should be saved, but not validated. + +First, we need to add the two buttons to the form:: + + $form = $this->createFormBuilder($task) + // ... + ->add('nextStep', 'submit') + ->add('previousStep', 'submit') + ->getForm(); + +Then, we configure the button for returning to the previous step to run +specific validation groups. In this example, we want it to suppress validation, +so we set its ``validation_groups`` option to false:: + + $form = $this->createFormBuilder($task) + // ... + ->add('previousStep', 'submit', array( + 'validation_groups' => false, + )) + ->getForm(); + +Now the form will skip your validation constraints. It will still validate +basic integrity constraints, such as checking whether an uploaded file was too +large or whether you tried to submit text in a number field. + +.. seealso:: + + To see how to use a service to resolve ``validation_groups`` dynamically + read the :doc:`/form/validation_group_service_resolver` article. diff --git a/cookbook/form/create_custom_field_type.rst b/form/create_custom_field_type.rst similarity index 61% rename from cookbook/form/create_custom_field_type.rst rename to form/create_custom_field_type.rst index 91dfe580c5e..c8010b6e8d6 100644 --- a/cookbook/form/create_custom_field_type.rst +++ b/form/create_custom_field_type.rst @@ -7,7 +7,7 @@ How to Create a Custom Form Field Type Symfony comes with a bunch of core field types available for building forms. However there are situations where you may want to create a custom form field type for a specific purpose. This recipe assumes you need a field definition -that holds a person's gender, based on the existing choice field. This section +that holds a shipping option, based on the existing choice field. This section explains how the field is defined, how you can customize its layout and finally, how you can register it for use in your application. @@ -16,25 +16,27 @@ Defining the Field Type In order to create the custom field type, first you have to create the class representing the field. In this situation the class holding the field type -will be called ``GenderType`` and the file will be stored in the default location +will be called ``ShippingType`` and the file will be stored in the default location for form fields, which is ``\Form\Type``. Make sure the field extends :class:`Symfony\\Component\\Form\\AbstractType`:: - // src/AppBundle/Form/Type/GenderType.php + // src/AppBundle/Form/Type/ShippingType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolver; - class GenderType extends AbstractType + class ShippingType extends AbstractType { public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'choices' => array( - 'm' => 'Male', - 'f' => 'Female', - ) + 'Standard Shipping' => 'standard', + 'Expedited Shipping' => 'expedited', + 'Priority Shipping' => 'priority', + ), + 'choices_as_values' => true, )); } @@ -45,7 +47,7 @@ for form fields, which is ``\Form\Type``. Make sure the field extend public function getName() { - return 'gender'; + return 'app_shipping'; } } @@ -54,14 +56,14 @@ for form fields, which is ``\Form\Type``. Make sure the field extend The location of this file is not important - the ``Form\Type`` directory is just a convention. -Here, the return value of the ``getParent`` function indicates that you're +Here, the return value of the ``getParent()`` function indicates that you're extending the ``choice`` field type. This means that, by default, you inherit all of the logic and rendering of that field type. To see some of the logic, check out the `ChoiceType`_ class. There are three methods that are particularly important: ``buildForm()`` - Each field type has a ``buildForm`` method, which is where + Each field type has a ``buildForm()`` method, which is where you configure and build any field(s). Notice that this is the same method you use to setup *your* forms, and it works the same here. @@ -69,8 +71,8 @@ important: This method is used to set any extra variables you'll need when rendering your field in a template. For example, in `ChoiceType`_, a ``multiple`` variable is set and used in the template to set (or not - set) the ``multiple`` attribute on the ``select`` field. See `Creating a Template for the Field`_ - for more details. + set) the ``multiple`` attribute on the ``select`` field. See + `Creating a Template for the Field`_ for more details. .. versionadded:: 2.7 The ``configureOptions()`` method was introduced in Symfony 2.7. Previously, @@ -93,30 +95,30 @@ The ``getName()`` method returns an identifier which should be unique in your application. This is used in various places, such as when customizing how your form type will be rendered. -The goal of this field was to extend the choice type to enable selection of -a gender. This is achieved by fixing the ``choices`` to a list of possible -genders. +The goal of this field was to extend the choice type to enable selection of the +shipping type. This is achieved by fixing the ``choices`` to a list of available +shipping options. Creating a Template for the Field --------------------------------- Each field type is rendered by a template fragment, which is determined in part by the value of your ``getName()`` method. For more information, see -:ref:`cookbook-form-customization-form-themes`. +:ref:`form-customization-form-themes`. In this case, since the parent field is ``choice``, you don't *need* to do any work as the custom field type will automatically be rendered like a ``choice`` type. But for the sake of this example, suppose that when your field is "expanded" (i.e. radio buttons or checkboxes, instead of a select field), you want to always render it in a ``ul`` element. In your form theme template (see above -link for details), create a ``gender_widget`` block to handle this: +link for details), create a ``shipping_widget`` block to handle this: .. configuration-block:: - .. code-block:: html+jinja + .. code-block:: html+twig - {# app/Resources/views/Form/fields.html.twig #} - {% block gender_widget %} + {# app/Resources/views/form/fields.html.twig #} + {% block shipping_widget %} {% spaceless %} {% if expanded %}
    @@ -136,7 +138,7 @@ link for details), create a ``gender_widget`` block to handle this: .. code-block:: html+php - +
      block($form, 'widget_container_attributes') ?>> @@ -151,10 +153,18 @@ link for details), create a ``gender_widget`` block to handle this: renderBlock('choice_widget') ?> +.. tip:: + + You can further customize the template used to render each children of the + choice type. The block to override in that case is named "block name" + + ``_entry`` + "element name" (``label``, ``errors`` or ``widget``) (e.g. to + customize the labels of the children of the Shipping widget you'd need to + define ``{% block shipping_entry_label %} ... {% endblock %}``). + .. note:: Make sure the correct widget prefix is used. In this example the name should - be ``gender_widget``, according to the value returned by ``getName``. + be ``shipping_widget``, according to the value returned by ``getName()``. Further, the main config file should point to the custom form template so that it's used when rendering all forms. @@ -167,21 +177,31 @@ link for details), create a ``gender_widget`` block to handle this: # app/config/config.yml twig: form_themes: - - 'AppBundle:Form:fields.html.twig' + - 'form/fields.html.twig' .. code-block:: xml - - AppBundle:Form:fields.html.twig - + + + + + form/fields.html.twig + + .. code-block:: php // app/config/config.php $container->loadFromExtension('twig', array( 'form_themes' => array( - 'AppBundle:Form:fields.html.twig', + 'form/fields.html.twig', ), )); @@ -196,7 +216,7 @@ link for details), create a ``gender_widget`` block to handle this: templating: form: resources: - - 'AppBundle:Form' + - ':form:fields.html.php' .. code-block:: xml @@ -211,7 +231,7 @@ link for details), create a ``gender_widget`` block to handle this: - AppBundle:Form + :form:fields.html.php @@ -224,7 +244,7 @@ link for details), create a ``gender_widget`` block to handle this: 'templating' => array( 'form' => array( 'resources' => array( - 'AppBundle:Form', + ':form:fields.html.php', ), ), ), @@ -242,25 +262,25 @@ new instance of the type in one of your forms:: use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; - class AuthorType extends AbstractType + class OrderType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('gender_code', new GenderType(), array( - 'placeholder' => 'Choose a gender', + $builder->add('shipping_code', new ShippingType(), array( + 'placeholder' => 'Choose a delivery option', )); } } -But this only works because the ``GenderType()`` is very simple. What if -the gender codes were stored in configuration or in a database? The next +But this only works because the ``ShippingType()`` is very simple. What if +the shipping codes were stored in configuration or in a database? The next section explains how more complex field types solve this problem. .. versionadded:: 2.6 - The ``placeholder`` option was introduced in Symfony 2.6 in favor of + The ``placeholder`` option was introduced in Symfony 2.6 and replaces ``empty_value``, which is available prior to 2.6. -.. _form-cookbook-form-field-service: +.. _form-field-service: Creating your Field Type as a Service ------------------------------------- @@ -268,7 +288,7 @@ Creating your Field Type as a Service So far, this entry has assumed that you have a very simple custom field type. But if you need access to configuration, a database connection, or some other service, then you'll want to register your custom type as a service. For -example, suppose that you're storing the gender parameters in configuration: +example, suppose that you're storing the shipping parameters in configuration: .. configuration-block:: @@ -276,29 +296,41 @@ example, suppose that you're storing the gender parameters in configuration: # app/config/config.yml parameters: - genders: - m: Male - f: Female + shipping_options: + standard: Standard Shipping + expedited: Expedited Shipping + priority: Priority Shipping .. code-block:: xml - - - Male - Female - - + + + + + + Standard Shipping + Expedited Shipping + Priority Shipping + + + .. code-block:: php // app/config/config.php - $container->setParameter('genders.m', 'Male'); - $container->setParameter('genders.f', 'Female'); + $container->setParameter('shipping_options', array( + 'standard' => 'Standard Shipping', + 'expedited' => 'Expedited Shipping', + 'priority' => 'Priority Shipping', + )); -To use the parameter, define your custom field type as a service, injecting -the ``genders`` parameter value as the first argument to its to-be-created -``__construct`` function: +To use the parameter, define your custom field type as a service, injecting the +``shipping_options`` parameter value as the first argument to its to-be-created +``__construct()`` function: .. configuration-block:: @@ -306,35 +338,40 @@ the ``genders`` parameter value as the first argument to its to-be-created # src/AppBundle/Resources/config/services.yml services: - app.form.type.gender: - class: AppBundle\Form\Type\GenderType + app.form.type.shipping: + class: AppBundle\Form\Type\ShippingType arguments: - - "%genders%" + - '%shipping_options%' tags: - - { name: form.type, alias: gender } + - { name: form.type, alias: app_shipping } .. code-block:: xml - - %genders% - - + + + + + + %shipping_options% + + + + .. code-block:: php // src/AppBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; + use AppBundle\Form\Type\ShippingType; - $container - ->setDefinition('app.form.type.gender', new Definition( - 'AppBundle\Form\Type\GenderType', - array('%genders%') - )) + $container->register('app.form.type.shipping', ShippingType::class) + ->addArgument('%shipping_options%') ->addTag('form.type', array( - 'alias' => 'gender', - )) - ; + 'alias' => 'app_shipping', + )); .. tip:: @@ -342,11 +379,11 @@ the ``genders`` parameter value as the first argument to its to-be-created for details. Be sure that the ``alias`` attribute of the tag corresponds with the value -returned by the ``getName`` method defined earlier. You'll see the importance +returned by the ``getName()`` method defined earlier. You'll see the importance of this in a moment when you use the custom field type. But first, add a ``__construct`` -method to ``GenderType``, which receives the gender configuration:: +method to ``ShippingType``, which receives the shipping configuration:: - // src/AppBundle/Form/Type/GenderType.php + // src/AppBundle/Form/Type/ShippingType.php namespace AppBundle\Form\Type; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -354,48 +391,49 @@ method to ``GenderType``, which receives the gender configuration:: // ... // ... - class GenderType extends AbstractType + class ShippingType extends AbstractType { - private $genderChoices; + private $shippingOptions; - public function __construct(array $genderChoices) + public function __construct(array $shippingOptions) { - $this->genderChoices = $genderChoices; + $this->shippingOptions = $shippingOptions; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( - 'choices' => $this->genderChoices, + 'choices' => array_flip($this->shippingOptions), + 'choices_as_values' => true, )); } // ... } -Great! The ``GenderType`` is now fueled by the configuration parameters and -registered as a service. Additionally, because you used the ``form.type`` alias in its +Great! The ``ShippingType`` is now fueled by the configuration parameters and +registered as a service. Additionally, because you used the ``form.type`` tag in its configuration, using the field is now much easier:: - // src/AppBundle/Form/Type/AuthorType.php + // src/AppBundle/Form/Type/OrderType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\FormBuilderInterface; // ... - class AuthorType extends AbstractType + class OrderType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('gender_code', 'gender', array( - 'placeholder' => 'Choose a gender', + $builder->add('shipping_code', 'app_shipping', array( + 'placeholder' => 'Choose a delivery option', )); } } Notice that instead of instantiating a new instance, you can just refer to -it by the alias used in your service configuration, ``gender``. Have fun! +it by the alias used in your service configuration, ``app_shipping``. Have fun! .. _`ChoiceType`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php .. _`FieldType`: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php diff --git a/cookbook/form/create_form_type_extension.rst b/form/create_form_type_extension.rst similarity index 67% rename from cookbook/form/create_form_type_extension.rst rename to form/create_form_type_extension.rst index ab1703c7596..451af9e7cc0 100644 --- a/cookbook/form/create_form_type_extension.rst +++ b/form/create_form_type_extension.rst @@ -5,7 +5,7 @@ How to Create a Form Type Extension =================================== :doc:`Custom form field types ` are great when -you need field types with a specific purpose, such as a gender selector, +you need field types with a specific purpose, such as a shipping type selector, or a VAT number input. But sometimes, you don't really need to add new field types - you want @@ -14,16 +14,15 @@ extensions come in. Form type extensions have 2 main use-cases: -#. You want to add a **generic feature to several types** (such as - adding a "help" text to every field type); #. You want to add a **specific feature to a single type** (such - as adding a "download" feature to the "file" field type). + as adding a "download" feature to the "file" field type); +#. You want to add a **generic feature to several types** (such as + adding a "help" text to every "input text"-like type). -In both those cases, it might be possible to achieve your goal with custom -form rendering, or custom form field types. But using form type extensions -can be cleaner (by limiting the amount of business logic in templates) -and more flexible (you can add several type extensions to a single form -type). +It might be possible to achieve your goal with custom form rendering or custom +form field types. But using form type extensions can be cleaner (by limiting the +amount of business logic in templates) and more flexible (you can add several +type extensions to a single form type). Form type extensions can achieve most of what custom field types can do, but instead of being field types of their own, **they plug into existing types**. @@ -48,8 +47,8 @@ When creating a form type extension, you can either implement the or extend the :class:`Symfony\\Component\\Form\\AbstractTypeExtension` class. In most cases, it's easier to extend the abstract class:: - // src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php - namespace Acme\DemoBundle\Form\Extension; + // src/AppBundle/Form/Extension/ImageTypeExtension.php + namespace AppBundle\Form\Extension; use Symfony\Component\Form\AbstractTypeExtension; @@ -66,17 +65,17 @@ class. In most cases, it's easier to extend the abstract class:: } } -The only method you **must** implement is the ``getExtendedType`` function. +The only method you **must** implement is the ``getExtendedType()`` function. It is used to indicate the name of the form type that will be extended by your extension. .. tip:: - The value you return in the ``getExtendedType`` method corresponds - to the value returned by the ``getName`` method in the form type class + The value you return in the ``getExtendedType()`` method corresponds + to the value returned by the ``getName()`` method in the form type class you wish to extend. -In addition to the ``getExtendedType`` function, you will probably want +In addition to the ``getExtendedType()`` function, you will probably want to override one of the following methods: * ``buildForm()`` @@ -88,8 +87,7 @@ to override one of the following methods: * ``finishView()`` For more information on what those methods do, you can refer to the -:doc:`Creating Custom Field Types ` -cookbook article. +:doc:`/form/create_custom_field_type` article. Registering your Form Type Extension as a Service ------------------------------------------------- @@ -103,27 +101,36 @@ tag: .. code-block:: yaml services: - acme_demo_bundle.image_type_extension: - class: Acme\DemoBundle\Form\Extension\ImageTypeExtension + app.image_type_extension: + class: AppBundle\Form\Extension\ImageTypeExtension tags: - { name: form.type_extension, alias: file } .. code-block:: xml - - - + + + + + + + + + .. code-block:: php + use AppBundle\Form\Extension\ImageTypeExtension; + $container - ->register( - 'acme_demo_bundle.image_type_extension', - 'Acme\DemoBundle\Form\Extension\ImageTypeExtension' - ) - ->addTag('form.type_extension', array('alias' => 'file')); + ->register('app.image_type_extension', ImageTypeExtension::class) + ->addTag('form.type_extension', array('alias' => 'file')) + ; The ``alias`` key of the tag is the type of field that this extension should be applied to. In your case, as you want to extend the ``file`` field type, @@ -135,13 +142,12 @@ Adding the extension Business Logic The goal of your extension is to display nice images next to file inputs (when the underlying model contains images). For that purpose, suppose that you use an approach similar to the one described in -:doc:`How to handle File Uploads with Doctrine `: -you have a Media model with a file property (corresponding to the file field -in the form) and a path property (corresponding to the image path in the -database):: +:doc:`How to handle File Uploads with Doctrine `: +you have a Media model with a path property, corresponding to the image path in +the database:: - // src/Acme/DemoBundle/Entity/Media.php - namespace Acme\DemoBundle\Entity; + // src/AppBundle/Entity/Media.php + namespace AppBundle\Entity; use Symfony\Component\Validator\Constraints as Assert; @@ -154,12 +160,6 @@ database):: */ private $path; - /** - * @var \Symfony\Component\HttpFoundation\File\UploadedFile - * @Assert\File(maxSize="2M") - */ - public $file; - // ... /** @@ -178,18 +178,18 @@ database):: Your form type extension class will need to do two things in order to extend the ``file`` form type: -#. Override the ``configureOptions`` method in order to add an ``image_path`` +#. Override the ``configureOptions()`` method in order to add an ``image_path`` option; -#. Override the ``buildForm`` and ``buildView`` methods in order to pass the image - URL to the view. +#. Override the ``buildView()`` methods in order to pass the image URL to the + view. The logic is the following: when adding a form field of type ``file``, you will be able to specify a new option: ``image_path``. This option will tell the file field how to get the actual image URL in order to display it in the view:: - // src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php - namespace Acme\DemoBundle\Form\Extension; + // src/AppBundle/Form/Extension/ImageTypeExtension.php + namespace AppBundle\Form\Extension; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormView; @@ -228,17 +228,16 @@ it in the view:: */ public function buildView(FormView $view, FormInterface $form, array $options) { - if (array_key_exists('image_path', $options)) { + if (isset($options['image_path'])) { $parentData = $form->getParent()->getData(); + $imageUrl = null; if (null !== $parentData) { $accessor = PropertyAccess::createPropertyAccessor(); $imageUrl = $accessor->getValue($parentData, $options['image_path']); - } else { - $imageUrl = null; } - // set an "image_url" variable that will be available when rendering this field + // sets an "image_url" variable that will be available when rendering this field $view->vars['image_url'] = $imageUrl; } } @@ -250,7 +249,7 @@ Override the File Widget Template Fragment Each field type is rendered by a template fragment. Those template fragments can be overridden in order to customize form rendering. For more information, -you can refer to the :ref:`cookbook-form-customization-form-themes` article. +you can refer to the :ref:`form-customization-form-themes` article. In your extension class, you have added a new variable (``image_url``), but you still need to take advantage of this new variable in your templates. @@ -258,9 +257,9 @@ Specifically, you need to override the ``file_widget`` block: .. configuration-block:: - .. code-block:: html+jinja + .. code-block:: html+twig - {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} + {# src/AppBundle/Resources/views/Form/fields.html.twig #} {% extends 'form_div_layout.html.twig' %} {% block file_widget %} @@ -276,7 +275,7 @@ Specifically, you need to override the ``file_widget`` block: .. code-block:: html+php - + widget($form) ?> @@ -286,7 +285,7 @@ Specifically, you need to override the ``file_widget`` block: You will need to change your config file or explicitly specify how you want your form to be themed in order for Symfony to use your overridden - block. See :ref:`cookbook-form-customization-form-themes` for more + block. See :ref:`form-customization-form-themes` for more information. Using the Form Type Extension @@ -296,8 +295,8 @@ From now on, when adding a field of type ``file`` in your form, you can specify an ``image_path`` option that will be used to display an image next to the file field. For example:: - // src/Acme/DemoBundle/Form/Type/MediaType.php - namespace Acme\DemoBundle\Form\Type; + // src/AppBundle/Form/Type/MediaType.php + namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -313,9 +312,25 @@ next to the file field. For example:: public function getName() { - return 'media'; + return 'app_media'; } } When displaying the form, if the underlying model has already been associated with an image, you will see it displayed next to the file input. + +Generic Form Type Extensions +---------------------------- + +You can modify several form types at once by specifying their common parent +(:doc:`/reference/forms/types`). For example, several form types natively +available in Symfony inherit from the ``text`` form type (such as ``email``, +``search``, ``url``, etc.). A form type extension applying to ``text`` +(i.e. whose ``getExtendedType()`` method returns ``text``) would apply to all of +these form types. + +In the same way, since **most** form types natively available in Symfony inherit +from the ``form`` form type, a form type extension applying to ``form`` would +apply to all of these. A notable exception are the ``button`` form types. Also +keep in mind that a custom form type which extends neither the ``form`` nor +the ``button`` type could always be created. diff --git a/form/csrf_protection.rst b/form/csrf_protection.rst new file mode 100644 index 00000000000..266555579c0 --- /dev/null +++ b/form/csrf_protection.rst @@ -0,0 +1,74 @@ +.. index:: + single: Forms; CSRF protection + +How to Implement CSRF Protection +================================ + +CSRF - or `Cross-site request forgery`_ - is a method by which a malicious +user attempts to make your legitimate users unknowingly submit data that +they don't intend to submit. Fortunately, CSRF attacks can be prevented by +using a CSRF token inside your forms. + +The good news is that, by default, Symfony embeds and validates CSRF tokens +automatically for you. This means that you can take advantage of the CSRF +protection without doing anything. In fact, every form in this article has +taken advantage of the CSRF protection! + +CSRF protection works by adding a hidden field to your form - called ``_token`` +by default - that contains a value that only you and your user knows. This +ensures that the user - not some other entity - is submitting the given data. +Symfony automatically validates the presence and accuracy of this token. + +The ``_token`` field is a hidden field and will be automatically rendered +if you include the ``form_end()`` function in your template, which ensures +that all un-rendered fields are output. + +.. caution:: + + Since the token is stored in the session, a session is started automatically + as soon as you render a form with CSRF protection. + +The CSRF token can be customized on a form-by-form basis. For example:: + + // ... + use AppBundle\Entity\Task; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class TaskType extends AbstractType + { + // ... + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => Task::class, + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + // a unique key to help generate the secret token + 'csrf_token_id' => 'task_item', + )); + } + + // ... + } + +.. _form-disable-csrf: + +To disable CSRF protection, set the ``csrf_protection`` option to false. +Customizations can also be made globally in your project. For more information, +see the :ref:`form configuration reference ` +section. + +.. note:: + + The ``csrf_token_id`` option is optional but greatly enhances the security + of the generated token by making it different for each form. + +.. caution:: + + CSRF tokens are meant to be different for every user. This is why you + need to be cautious if you try to cache pages with forms including this + kind of protection. For more information, see + :doc:`/http_cache/form_csrf_caching`. + +.. _`Cross-site request forgery`: http://en.wikipedia.org/wiki/Cross-site_request_forgery diff --git a/form/data_based_validation.rst b/form/data_based_validation.rst new file mode 100644 index 00000000000..3e31be7cd67 --- /dev/null +++ b/form/data_based_validation.rst @@ -0,0 +1,75 @@ +.. index:: + single: Forms; Validation groups based on submitted data + +How to Choose Validation Groups Based on the Submitted Data +=========================================================== + +If you need some advanced logic to determine the validation groups (e.g. +based on submitted data), you can set the ``validation_groups`` option +to an array callback:: + + use AppBundle\Entity\Client; + use Symfony\Component\OptionsResolver\OptionsResolver; + + // ... + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'validation_groups' => array( + Client::class, + 'determineValidationGroups', + ), + )); + } + +This will call the static method ``determineValidationGroups()`` on the +``Client`` class after the form is submitted, but before validation is executed. +The Form object is passed as an argument to that method (see next example). +You can also define whole logic inline by using a ``Closure``:: + + use AppBundle\Entity\Client; + use Symfony\Component\Form\FormInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + // ... + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'validation_groups' => function (FormInterface $form) { + $data = $form->getData(); + + if (Client::TYPE_PERSON == $data->getType()) { + return array('person'); + } + + return array('company'); + }, + )); + } + +Using the ``validation_groups`` option overrides the default validation +group which is being used. If you want to validate the default constraints +of the entity as well you have to adjust the option as follows:: + + use AppBundle\Entity\Client; + use Symfony\Component\Form\FormInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + // ... + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'validation_groups' => function (FormInterface $form) { + $data = $form->getData(); + + if (Client::TYPE_PERSON == $data->getType()) { + return array('Default', 'person'); + } + + return array('Default', 'company'); + }, + )); + } + +You can find more information about how the validation groups and the default constraints +work in the article about :doc:`validation groups `. diff --git a/cookbook/form/data_transformers.rst b/form/data_transformers.rst similarity index 79% rename from cookbook/form/data_transformers.rst rename to form/data_transformers.rst index 0d09535b5bf..74c3c7e2f34 100644 --- a/cookbook/form/data_transformers.rst +++ b/form/data_transformers.rst @@ -16,14 +16,17 @@ to render the form, and then back into a ``DateTime`` object on submit. When a form field has the ``inherit_data`` option set, Data Transformers won't be applied to that field. -Simple Example: Sanitizing HTML on User Input ---------------------------------------------- +.. _simple-example-sanitizing-html-on-user-input: -Suppose you have a Task form with a description ``textarea`` type:: +Simple Example: Transforming String Tags from User Input to an Array +-------------------------------------------------------------------- - // src/AppBundle/Form/TaskType.php +Suppose you have a Task form with a tags ``text`` type:: + + // src/AppBundle/Form/Type/TaskType.php namespace AppBundle\Form\Type; + use AppBundle\Entity\Task; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -32,32 +35,27 @@ Suppose you have a Task form with a description ``textarea`` type:: { public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('description', 'textarea'); + $builder->add('tags', 'text'); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( - 'data_class' => 'AppBundle\Entity\Task', + 'data_class' => Task::class, )); } // ... } -But, there are two complications: - -#. Your users are allowed to use *some* HTML tags, but not others: you need a way - to call :phpfunction:`striptags` after the form is submitted; - -#. To be friendly, you want to convert ``
      `` tags into line breaks (``\n``) before - rendering the field so the text is easier to edit. +Internally the ``tags`` are stored as an array, but displayed to the user as a +simple comma separated string to make them easier to edit. -This is a *perfect* time to attach a custom data transformer to the ``description`` +This is a *perfect* time to attach a custom data transformer to the ``tags`` field. The easiest way to do this is with the :class:`Symfony\\Component\\Form\\CallbackTransformer` class:: - // src/AppBundle/Form/TaskType.php + // src/AppBundle/Form/Type/TaskType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\CallbackTransformer; @@ -68,20 +66,17 @@ class:: { public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('description', 'textarea'); + $builder->add('tags', 'text'); - $builder->get('description') + $builder->get('tags') ->addModelTransformer(new CallbackTransformer( - // transform
      to \n so the textarea reads easier - function ($originalDescription) { - return preg_replace('##i', "\n", $originalDescription); + function ($tagsAsArray) { + // transform the array to a string + return implode(', ', $tagsAsArray); }, - function ($submittedDescription) { - // remove most HTML tags (but not br,p) - $cleaned = strip_tags($submittedDescription, '

      '); - - // transform any \n to real
      - return str_replace("\n", '
      ', $cleaned); + function ($tagsAsString) { + // transform the string back to an array + return explode(', ', $tagsAsString); } )) ; @@ -90,10 +85,10 @@ class:: // ... } -The ``CallbackTransformer`` takes two callback functions as arguments. The first transforms -the original value into a format that'll be used to render the field. The second -does the reverse: it transforms the submitted value back into the format you'll use -in your code. +The ``CallbackTransformer`` takes two callback functions as arguments. The +first transforms the original value into a format that'll be used to render the +field. The second does the reverse: it transforms the submitted value back into +the format you'll use in your code. .. tip:: @@ -105,7 +100,8 @@ You can also add the transformer, right when adding the field by changing the fo slightly:: $builder->add( - $builder->create('description', 'textarea') + $builder + ->create('tags', 'text') ->addModelTransformer(...) ); @@ -120,9 +116,11 @@ issue number. Start by setting up the text field like normal:: - // src/AppBundle/Form/TaskType.php + // src/AppBundle/Form/Type/TaskType.php namespace AppBundle\Form\Type; + use AppBundle\Entity\Task; + // ... class TaskType extends AbstractType { @@ -137,7 +135,7 @@ Start by setting up the text field like normal:: public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( - 'data_class' => 'AppBundle\Entity\Task' + 'data_class' => Task::class, )); } @@ -161,17 +159,17 @@ to and from the issue number and the ``Issue`` object:: namespace AppBundle\Form\DataTransformer; use AppBundle\Entity\Issue; - use Doctrine\Common\Persistence\EntityManager; + use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; class IssueToNumberTransformer implements DataTransformerInterface { - private $entityManager; + private $objectManager; - public function __construct(EntityManager $entityManager) + public function __construct(ObjectManager $objectManager) { - $this->entityManager = $entityManager; + $this->objectManager = $objectManager; } /** @@ -203,8 +201,8 @@ to and from the issue number and the ``Issue`` object:: return; } - $issue = $this->entityManager - ->getRepository('AppBundle:Issue') + $issue = $this->objectManager + ->getRepository(Issue::class) // query for the issue with this id ->find($issueNumber) ; @@ -249,20 +247,20 @@ of the entity manager (because ``IssueToNumberTransformer`` needs this). No problem! Just add a ``__construct()`` function to ``TaskType`` and force this to be passed in. Then, you can easily create and add the transformer:: - // src/AppBundle/Form/TaskType.php + // src/AppBundle/Form/Type/TaskType.php namespace AppBundle\Form\Type; use AppBundle\Form\DataTransformer\IssueToNumberTransformer; - use Doctrine\Common\Persistence\EntityManager; + use Doctrine\Common\Persistence\ObjectManager; // ... class TaskType extends AbstractType { - private $entityManager; + private $objectManager; - public function __construct(EntityManager $entityManager) + public function __construct(ObjectManager $objectManager) { - $this->entityManager = $entityManager; + $this->objectManager = $objectManager; } public function buildForm(FormBuilderInterface $builder, array $options) @@ -277,7 +275,7 @@ to be passed in. Then, you can easily create and add the transformer:: // ... $builder->get('issue') - ->addModelTransformer(new IssueToNumberTransformer($this->entityManager)); + ->addModelTransformer(new IssueToNumberTransformer($this->objectManager)); } // ... @@ -294,7 +292,7 @@ Now, when you create your ``TaskType``, you'll need to pass in the entity manage .. note:: To make this step easier (especially if ``TaskType`` is embedded into other - form type classes), you might choose to :ref:`register your form type as a service `. + form type classes), you might choose to :doc:`register your form type as a service `. Cool, you're done! Your user will be able to enter an issue number into the text field and it will be transformed back into an Issue object. This means @@ -322,7 +320,7 @@ Creating a Reusable issue_selector Field In the above example, you applied the transformer to a normal ``text`` field. But if you do this transformation a lot, it might be better to -:doc:`create a custom field type `. +:doc:`create a custom field type `. that does this automatically. First, create the custom field type class:: @@ -331,23 +329,23 @@ First, create the custom field type class:: namespace AppBundle\Form; use AppBundle\Form\DataTransformer\IssueToNumberTransformer; - use Doctrine\ORM\EntityManager; + use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class IssueSelectorType extends AbstractType { - private $entityManager; + private $objectManager; - public function __construct(EntityManager $entityManager) + public function __construct(ObjectManager $objectManager) { - $this->entityManager = $entityManager; + $this->objectManager = $objectManager; } public function buildForm(FormBuilderInterface $builder, array $options) { - $transformer = new IssueToNumberTransformer($this->entityManager); + $transformer = new IssueToNumberTransformer($this->objectManager); $builder->addModelTransformer($transformer); } @@ -383,11 +381,10 @@ it's recognized as a custom field type: services: app.type.issue_selector: class: AppBundle\Form\IssueSelectorType - arguments: ["@doctrine.orm.entity_manager"] + arguments: ['@doctrine.orm.entity_manager'] tags: - { name: form.type, alias: issue_selector } - .. code-block:: xml @@ -409,27 +406,18 @@ it's recognized as a custom field type: .. code-block:: php // app/config/services.php - use Symfony\Component\DependencyInjection\Definition; + use AppBundle\Form\IssueSelectorType; use Symfony\Component\DependencyInjection\Reference; // ... - $container - ->setDefinition('app.type.issue_selector', new Definition( - 'AppBundle\Form\IssueSelectorType' - ), - array( - new Reference('doctrine.orm.entity_manager'), - ) - ) - ->addTag('form.type', array( - 'alias' => 'issue_selector', - )) - ; + $container->register('app.type.issue_selector', IssueSelectorType::class) + ->addArgument(new Reference('doctrine.orm.entity_manager')) + ->addTag('form.type'); Now, whenever you need to use your special ``issue_selector`` field type, it's quite easy:: - // src/AppBundle/Form/TaskType.php + // src/AppBundle/Form/Type/TaskType.php namespace AppBundle\Form\Type; use AppBundle\Form\DataTransformer\IssueToNumberTransformer; @@ -457,7 +445,7 @@ In the above example, the transformer was used as a "model" transformer. In fact, there are two different types of transformers and three different types of underlying data. -.. image:: /images/cookbook/form/DataTransformersTypes.png +.. image:: /_images/form/data-transformer-types.png :align: center In any form, the three different types of data are: @@ -478,16 +466,16 @@ The two different types of transformers help convert to and from each of these types of data: **Model transformers**: - - ``transform``: "model data" => "norm data" - - ``reverseTransform``: "norm data" => "model data" + - ``transform()``: "model data" => "norm data" + - ``reverseTransform()``: "norm data" => "model data" **View transformers**: - - ``transform``: "norm data" => "view data" - - ``reverseTransform``: "view data" => "norm data" + - ``transform()``: "norm data" => "view data" + - ``reverseTransform()``: "view data" => "norm data" Which transformer you need depends on your situation. -To use the view transformer, call ``addViewTransformer``. +To use the view transformer, call ``addViewTransformer()``. So why Use the Model Transformer? --------------------------------- diff --git a/cookbook/form/direct_submit.rst b/form/direct_submit.rst similarity index 72% rename from cookbook/form/direct_submit.rst rename to form/direct_submit.rst index 7166210319b..464bd26cdd7 100644 --- a/cookbook/form/direct_submit.rst +++ b/form/direct_submit.rst @@ -22,29 +22,26 @@ submissions:: $form->handleRequest($request); - if ($form->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { // perform some action... return $this->redirectToRoute('task_success'); } - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( + return $this->render('default/new.html.twig', array( 'form' => $form->createView(), )); } .. tip:: - To see more about this method, read :ref:`book-form-handling-form-submissions`. + To see more about this method, read :ref:`form-handling-form-submissions`. -.. _cookbook-form-call-submit-directly: +.. _form-call-submit-directly: Calling Form::submit() manually ------------------------------- -.. versionadded:: 2.3 - Before Symfony 2.3, the ``submit()`` method was known as ``bind()``. - In some cases, you want better control over when exactly your form is submitted and what data is passed to it. Instead of using the :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` @@ -63,14 +60,14 @@ method, pass the submitted data directly to if ($request->isMethod('POST')) { $form->submit($request->request->get($form->getName())); - if ($form->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { // perform some action... return $this->redirectToRoute('task_success'); } } - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( + return $this->render('default/new.html.twig', array( 'form' => $form->createView(), )); } @@ -84,13 +81,27 @@ method, pass the submitted data directly to $form->get('firstName')->submit('Fabien'); -.. _cookbook-form-submit-request: +.. tip:: + + When submitting a form via a "PATCH" request, you may want to update only a few + submitted fields. To achieve this, you may pass an optional second boolean + argument to ``submit()``. Passing ``false`` will remove any missing fields + within the form object. Otherwise, the missing fields will be set to ``null``. + +.. caution:: + + When the second parameter ``$clearMissing`` is ``false``, like with the + "PATCH" method, the validation extension will only handle the submitted + fields. If the underlying data needs to be validated, this should be done + manually, i.e. using the validator. + +.. _form-submit-request: Passing a Request to Form::submit() (Deprecated) ------------------------------------------------ .. versionadded:: 2.3 - Before Symfony 2.3, the ``submit`` method was known as ``bind``. + Before Symfony 2.3, the ``submit()`` method was known as ``bind()``. Before Symfony 2.3, the :method:`Symfony\\Component\\Form\\FormInterface::submit` method accepted a :class:`Symfony\\Component\\HttpFoundation\\Request` object as @@ -108,19 +119,19 @@ a convenient shortcut to the previous example:: if ($request->isMethod('POST')) { $form->submit($request); - if ($form->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { // perform some action... return $this->redirectToRoute('task_success'); } } - return $this->render('AcmeTaskBundle:Default:new.html.twig', array( + return $this->render('default/new.html.twig', array( 'form' => $form->createView(), )); } Passing the :class:`Symfony\\Component\\HttpFoundation\\Request` directly to :method:`Symfony\\Component\\Form\\FormInterface::submit` still works, but is -deprecated and will be removed in Symfony 3.0. You should use the method +deprecated and has been removed in Symfony 3.0. You should use the method :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` instead. diff --git a/form/disabling_validation.rst b/form/disabling_validation.rst new file mode 100644 index 00000000000..dafbd684b21 --- /dev/null +++ b/form/disabling_validation.rst @@ -0,0 +1,29 @@ +.. index:: + single: Forms; Disabling validation + +How to Disable the Validation of Submitted Data +=============================================== + +.. versionadded:: 2.3 + The ability to set ``validation_groups`` to false was introduced in Symfony 2.3. + +Sometimes it is useful to suppress the validation of a form altogether. For +these cases you can set the ``validation_groups`` option to ``false``:: + + use Symfony\Component\OptionsResolver\OptionsResolver; + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'validation_groups' => false, + )); + } + +Note that when you do that, the form will still run basic integrity checks, +for example whether an uploaded file was too large or whether non-existing +fields were submitted. + +The submission of extra form fields can be controlled with the +:ref:`allow_extra_fields config option ` and +the maximum upload file size should be handled via your PHP and web server +configuration. diff --git a/cookbook/form/dynamic_form_modification.rst b/form/dynamic_form_modification.rst similarity index 84% rename from cookbook/form/dynamic_form_modification.rst rename to form/dynamic_form_modification.rst index 72d03284540..f2cf659a018 100644 --- a/cookbook/form/dynamic_form_modification.rst +++ b/form/dynamic_form_modification.rst @@ -7,38 +7,38 @@ How to Dynamically Modify Forms Using Form Events Often times, a form can't be created statically. In this entry, you'll learn how to customize your form based on three common use-cases: -1) :ref:`cookbook-form-events-underlying-data` +1) :ref:`form-events-underlying-data` Example: you have a "Product" form and need to modify/add/remove a field based on the data on the underlying Product being edited. -2) :ref:`cookbook-form-events-user-data` +2) :ref:`form-events-user-data` Example: you create a "Friend Message" form and need to build a drop-down that contains only users that are friends with the *current* authenticated user. -3) :ref:`cookbook-form-events-submitted-data` +3) :ref:`form-events-submitted-data` Example: on a registration form, you have a "country" field and a "state" field which should populate dynamically based on the value in the "country" field. If you wish to learn more about the basics behind form events, you can -take a look at the :doc:`Form Events ` -documentation. +take a look at the :doc:`Form Events ` documentation. -.. _cookbook-form-events-underlying-data: +.. _form-events-underlying-data: Customizing your Form Based on the Underlying Data -------------------------------------------------- -Before jumping right into dynamic form generation, hold on and recall what +Before starting with dynamic form generation, remember what a bare form class looks like:: // src/AppBundle/Form/Type/ProductType.php namespace AppBundle\Form\Type; + use AppBundle\Entity\Product; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -54,7 +54,7 @@ a bare form class looks like:: public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( - 'data_class' => 'AppBundle\Entity\Product' + 'data_class' => Product::class, )); } @@ -67,7 +67,7 @@ a bare form class looks like:: .. note:: If this particular section of code isn't already familiar to you, you - probably need to take a step back and first review the :doc:`Forms chapter ` + probably need to take a step back and first review the :doc:`Forms article ` before proceeding. Assume for a moment that this form utilizes an imaginary "Product" class @@ -77,13 +77,11 @@ or if an existing product is being edited (e.g. a product fetched from the datab Suppose now, that you don't want the user to be able to change the ``name`` value once the object has been created. To do this, you can rely on Symfony's -:doc:`EventDispatcher component ` +:doc:`EventDispatcher component ` system to analyze the data on the object and modify the form based on the Product object's data. In this entry, you'll learn how to add this level of flexibility to your forms. -.. _`cookbook-forms-event-listener`: - Adding an Event Listener to a Form Class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -111,7 +109,6 @@ creating that particular field is delegated to an event listener:: // ... } - The goal is to create a ``name`` field *only* if the underlying ``Product`` object is new (e.g. hasn't been persisted to the database). Based on that, the event listener might look like the following:: @@ -124,7 +121,7 @@ the event listener might look like the following:: $product = $event->getData(); $form = $event->getForm(); - // check if the Product object is "new" + // checks if the Product object is "new" // If no data is passed to the form, the data is "null". // This should be considered a new "Product" if (!$product || null === $product->getId()) { @@ -142,8 +139,6 @@ the event listener might look like the following:: full list of form events via the :class:`Symfony\\Component\\Form\\FormEvents` class. -.. _`cookbook-forms-event-subscriber`: - Adding an Event Subscriber to a Form Class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -199,8 +194,7 @@ class:: } } - -.. _cookbook-form-events-user-data: +.. _form-events-user-data: How to dynamically Generate Forms Based on user Data ---------------------------------------------------- @@ -240,7 +234,7 @@ Using an event listener, your form might look like this:: public function getName() { - return 'friend_message'; + return 'app_friend_message'; } } @@ -260,8 +254,8 @@ done in the constructor:: .. note:: You might wonder, now that you have access to the User (through the token - storage), why not just use it directly in ``buildForm`` and omit the - event listener? This is because doing so in the ``buildForm`` method + storage), why not just use it directly in ``buildForm()`` and omit the + event listener? This is because doing so in the ``buildForm()`` method would result in the whole form type being modified and not just this one form instance. This may not usually be a problem, but technically a single form type could be used on a single request to create many forms @@ -273,10 +267,11 @@ Customizing the Form Type Now that you have all the basics in place you can take advantage of the ``TokenStorageInterface`` and fill in the listener logic:: - // src/AppBundle/FormType/FriendMessageFormType.php + // src/AppBundle/Form/Type/FriendMessageFormType.php - use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + use AppBundle\Entity\User; use Doctrine\ORM\EntityRepository; + use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; // ... class FriendMessageFormType extends AbstractType @@ -309,8 +304,8 @@ and fill in the listener logic:: $form = $event->getForm(); $formOptions = array( - 'class' => 'AppBundle\Entity\User', - 'property' => 'fullName', + 'class' => User::class, + 'choice_label' => 'fullName', 'query_builder' => function (EntityRepository $er) use ($user) { // build a custom query // return $er->createQueryBuilder('u')->addOrderBy('fullName', 'DESC'); @@ -386,59 +381,66 @@ it with :ref:`dic-tags-form-type`. services: app.form.friend_message: class: AppBundle\Form\Type\FriendMessageFormType - arguments: ["@security.token_storage"] + arguments: ['@security.token_storage'] tags: - - { name: form.type, alias: friend_message } + - { name: form.type, alias: app_friend_message } .. code-block:: xml - - - - - - + + + + + + + + + + .. code-block:: php // app/config/config.php - $definition = new Definition('AppBundle\Form\Type\FriendMessageFormType'); - $definition->addTag('form.type', array('alias' => 'friend_message')); - $container->setDefinition( - 'app.form.friend_message', - $definition, - array('security.token_storage') - ); + use AppBundle\Form\Type\FriendMessageFormType; + use Symfony\Component\DependencyInjection\Reference; + + $container->register('app.form.friend_message', FriendMessageFormType::class) + ->addArgument(new Reference('security.token_storage')) + ->addTag('form.type', array('alias' => 'app_friend_message')); -If you wish to create it from within a controller or any other service that has -access to the form factory, you then use:: +If you wish to create it from within a service that has access to the form factory, +you then use:: - use Symfony\Component\DependencyInjection\ContainerAware; + $form = $formFactory->create('friend_message'); - class FriendMessageController extends ContainerAware +In a controller that extends the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` +class, you can simply call:: + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + + class FriendMessageController extends Controller { public function newAction(Request $request) { - $form = $this->get('form.factory')->create('friend_message'); + $form = $this->createForm('app_friend_message'); // ... } } -If you extend the ``Symfony\Bundle\FrameworkBundle\Controller\Controller`` class, you can simply call:: - - $form = $this->createForm('friend_message'); - You can also easily embed the form type into another form:: // inside some other "form type" class public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('message', 'friend_message'); + $builder->add('message', 'app_friend_message'); } -.. _cookbook-form-events-submitted-data: +.. _form-events-submitted-data: Dynamic Generation for Submitted Forms -------------------------------------- @@ -486,9 +488,10 @@ sport like this:: $positions = null === $sport ? array() : $sport->getAvailablePositions(); $form->add('position', 'entity', array( - 'class' => 'AppBundle:Position', + 'class' => 'AppBundle:Position', 'placeholder' => '', - 'choices' => $positions, + 'choices' => $positions, + 'choices_as_values' => true, )); } ); @@ -498,7 +501,7 @@ sport like this:: } .. versionadded:: 2.6 - The ``placeholder`` option was introduced in Symfony 2.6 in favor of + The ``placeholder`` option was introduced in Symfony 2.6 and replaces ``empty_value``, which is available prior to 2.6. When you're building this form to display to the user for the first time, @@ -549,9 +552,10 @@ The type would now look like:: $positions = null === $sport ? array() : $sport->getAvailablePositions(); $form->add('position', 'entity', array( - 'class' => 'AppBundle:Position', + 'class' => 'AppBundle:Position', 'placeholder' => '', - 'choices' => $positions, + 'choices' => $positions, + 'choices_as_values' => true, )); }; @@ -587,6 +591,11 @@ callbacks only because in two different scenarios, the data that you can use is available in different events. Other than that, the listeners always perform exactly the same things on a given form. +.. tip:: + + The ``FormEvents::POST_SUBMIT`` event does not allow to modify the form + the listener is bound to, but it allows to modify its parent. + One piece that is still missing is the client-side updating of your form after the sport is selected. This should be handled by making an AJAX call back to your application. Assume that you have a sport meetup creation controller:: @@ -607,12 +616,12 @@ your application. Assume that you have a sport meetup creation controller:: $meetup = new SportMeetup(); $form = $this->createForm(new SportMeetupType(), $meetup); $form->handleRequest($request); - if ($form->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { // ... save the meetup, redirect etc. } return $this->render( - 'AppBundle:Meetup:create.html.twig', + 'meetup/create.html.twig', array('form' => $form->createView()) ); } @@ -625,9 +634,9 @@ field according to the current selection in the ``sport`` field: .. configuration-block:: - .. code-block:: html+jinja + .. code-block:: html+twig - {# app/Resources/views/Meetup/create.html.twig #} + {# app/Resources/views/meetup/create.html.twig #} {{ form_start(form) }} {{ form_row(form.sport) }} {# `` +(``TextType``, ``FileType``, ``HiddenType``), ``