diff --git a/sites/all/modules/examples/examples/.eslintrc b/sites/all/modules/examples/examples/.eslintrc
new file mode 100644
index 000000000..7e88b38d0
--- /dev/null
+++ b/sites/all/modules/examples/examples/.eslintrc
@@ -0,0 +1,95 @@
+{
+ "extends": "eslint:recommended",
+ "env": {
+ "browser": true
+ },
+ "globals": {
+ "Drupal": true,
+ "drupalSettings": true,
+ "drupalTranslations": true,
+ "domready": true,
+ "jQuery": true,
+ "_": true,
+ "matchMedia": true,
+ "Backbone": true,
+ "Modernizr": true,
+ "CKEDITOR": true
+ },
+ "rules": {
+ // Errors.
+ "array-bracket-spacing": [2, "never"],
+ "block-scoped-var": 2,
+ "brace-style": [2, "stroustrup", {"allowSingleLine": true}],
+ "comma-dangle": [2, "never"],
+ "comma-spacing": 2,
+ "comma-style": [2, "last"],
+ "computed-property-spacing": [2, "never"],
+ "curly": [2, "all"],
+ "eol-last": 2,
+ "eqeqeq": [2, "smart"],
+ "guard-for-in": 2,
+ "indent": [2, 2, {"SwitchCase": 1}],
+ "key-spacing": [2, {"beforeColon": false, "afterColon": true}],
+ "keyword-spacing": [2, {"before": true, "after": true}],
+ "linebreak-style": [2, "unix"],
+ "lines-around-comment": [2, {"beforeBlockComment": true, "afterBlockComment": false}],
+ "new-parens": 2,
+ "no-array-constructor": 2,
+ "no-caller": 2,
+ "no-catch-shadow": 2,
+ "no-eval": 2,
+ "no-extend-native": 2,
+ "no-extra-bind": 2,
+ "no-extra-parens": [2, "functions"],
+ "no-implied-eval": 2,
+ "no-iterator": 2,
+ "no-label-var": 2,
+ "no-labels": 2,
+ "no-lone-blocks": 2,
+ "no-loop-func": 2,
+ "no-multi-spaces": 2,
+ "no-multi-str": 2,
+ "no-native-reassign": 2,
+ "no-nested-ternary": 2,
+ "no-new-func": 2,
+ "no-new-object": 2,
+ "no-new-wrappers": 2,
+ "no-octal-escape": 2,
+ "no-process-exit": 2,
+ "no-proto": 2,
+ "no-return-assign": 2,
+ "no-script-url": 2,
+ "no-sequences": 2,
+ "no-shadow-restricted-names": 2,
+ "no-spaced-func": 2,
+ "no-trailing-spaces": 2,
+ "no-undef-init": 2,
+ "no-undefined": 2,
+ "no-unused-expressions": 2,
+ "no-unused-vars": [2, {"vars": "all", "args": "none"}],
+ "no-with": 2,
+ "object-curly-spacing": [2, "never"],
+ "one-var": [2, "never"],
+ "quote-props": [2, "consistent-as-needed"],
+ "quotes": [2, "single", "avoid-escape"],
+ "semi": [2, "always"],
+ "semi-spacing": [2, {"before": false, "after": true}],
+ "space-before-blocks": [2, "always"],
+ "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
+ "space-in-parens": [2, "never"],
+ "space-infix-ops": 2,
+ "space-unary-ops": [2, { "words": true, "nonwords": false }],
+ "spaced-comment": [2, "always"],
+ "strict": 2,
+ "yoda": [2, "never"],
+ // Warnings.
+ "max-nested-callbacks": [1, 3],
+ "valid-jsdoc": [1, {
+ "prefer": {
+ "returns": "return",
+ "property": "prop"
+ },
+ "requireReturn": false
+ }]
+ }
+}
diff --git a/sites/all/modules/examples/examples/CONTRIBUTING.md b/sites/all/modules/examples/examples/CONTRIBUTING.md
new file mode 100644
index 000000000..a39f3a4dc
--- /dev/null
+++ b/sites/all/modules/examples/examples/CONTRIBUTING.md
@@ -0,0 +1,63 @@
+Drupal Examples For Developers: Contributor's Guide
+===================================================
+
+Examples for Developers is a community project.
+
+If you'd like to participate in Examples development, thank you!
+
+If you are new to Drupal or open source in general, have no fear. Examples is
+an easy-going project where you can learn some things about how to work on a
+collaborative project in a friendly environment.
+
+
+Policies
+--------
+
+Examples follows the Drupal core process as much as possible.
+
+Contributions thus need to be similar in quality to Drupal core patches.
+Contributions will need to meet the following minimum standards:
+
+### Normal Drupal issue process
+
+Drupal projects use patches related to issues. You should know how to make a
+patch and an interdiff using git. It's fine to develop on github or
+what-have-you, but eventually it has to be a patch that can be reviewed in the
+normal Drupal issue process. See the list of resources for some information on
+how to do do this.
+
+Your patch will also need to be reviewed by someone other than yourself. Learn
+about the review process in the resources section.
+
+### DrupalCI
+
+Examples uses the Drupal automated testing system to verify the applicability of
+patches. See `TESTING.md` for details.
+
+### Drupal coding standards
+
+All code in Examples should adhere to the Drupal core coding standards. Examples
+uses the Drupal Coder project and PHP_CodeSniffer to enforce coding standards.
+Think of this as another test your code must pass. See `STANDARDS.md` for
+details.
+
+
+Resources
+---------
+
+### Novice
+
+Drupal novice contribution guide: https://www.drupal.org/novice
+
+Drupal contribution guide: https://www.drupal.org/contribute
+
+What's a patch? https://www.drupal.org/patch
+
+How to make a patch with git: https://www.drupal.org/node/707484
+
+### Everyone
+
+How to review a patch: https://www.drupal.org/patch/review
+
+See `STANDARDS.md` and `TESTING.md` for information on how to run a coding
+standards test, and also how to run the tests themselves.
diff --git a/sites/all/modules/examples/examples/LICENSE.txt b/sites/all/modules/examples/examples/LICENSE.txt
new file mode 100644
index 000000000..d159169d1
--- /dev/null
+++ b/sites/all/modules/examples/examples/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/examples/examples/README.md b/sites/all/modules/examples/examples/README.md
new file mode 100644
index 000000000..1e4c25b38
--- /dev/null
+++ b/sites/all/modules/examples/examples/README.md
@@ -0,0 +1,79 @@
+Examples for Developers
+=======================
+
+Project site: http://drupal.org/project/examples
+
+Code: https://drupal.org/project/examples/git-instructions
+
+Issues: https://drupal.org/project/issues/examples
+
+What Is This?
+-------------
+
+This set of modules is intended to provide working examples of Drupal 8's
+features and APIs. The modules strive to be simple, well documented and
+modification friendly, in order to help developers quickly learn their inner
+workings.
+
+These examples are meant to teach you about code-level development for Drupal
+8. Some solutions might be better served using a contributed module, so that
+you don't end up having to re-invent the wheel in PHP. When in doubt, look for
+an existing contrib project that already does what you want, and contribute to
+that project.
+
+
+How To Use The Examples
+-----------------------
+
+There are three main ways to interact with the examples in this project:
+
+1. Enable the modules and use them within Drupal. Not all modules will have
+obvious things to see within your Drupal installation. For instance, while the
+Page and Form API examples will display forms, the Database API example does not
+have much that is visible within Drupal.
+
+2. Read the code. Much effort has gone into making the example code readable,
+not only in terms of the code itself, but also the extensive inline comments
+and documentation blocks.
+
+3. Browse the code and documentation on the web. There are two main places to
+do this:
+
+* https://api.drupal.org/api/examples is the main API site for all of Drupal.
+It has all manner of cross-linked references between the example code and the
+APIs being demonstrated. All of the Doxygen-based comments in the code are
+parsed and made browseable here.
+
+* http://drupalcode.org/project/examples.git allows you to browse the git
+repository for the Examples project.
+
+This project ships with a composer.json file. This is meant to illustrate how
+to provide a composer.json file for a Drupal contrib project. You can read more
+about how to use Composer with Drupal here: https://www.drupal.org/node/2718229
+
+How To Install The Modules
+--------------------------
+
+1. The Examples project installs like any other Drupal module. There is extensive
+documentation on how to do this here:
+https://drupal.org/documentation/install/modules-themes/modules-8 But essentially:
+Download the tarball and expand it into the modules/ directory in your Drupal 8
+installation.
+
+2. Within Drupal, enable any Example sub-module you wish to explore in Admin
+menu > Extend.
+
+3. Rebuild access permissions if you are prompted to.
+
+4. Profit! The links for Examples material will appear in your Tools menu. This
+menu appears on the left sidebar by default. You'll need to reenable it if you
+removed it.
+
+Having seen the behavior of the various example modules, you can move on to
+reading the code, experimenting with it, and hopefully grasp how things work.
+
+If you find a problem, incorrect comment, obsolete or improper code or such,
+please search for an issue about it at http://drupal.org/project/issues/examples
+If there isn't already an issue for it, please create a new one.
+
+Thanks.
diff --git a/sites/all/modules/examples/examples/STANDARDS.md b/sites/all/modules/examples/examples/STANDARDS.md
new file mode 100644
index 000000000..387656a33
--- /dev/null
+++ b/sites/all/modules/examples/examples/STANDARDS.md
@@ -0,0 +1,86 @@
+Developing with Coding Standards for Examples for Developers
+============================================================
+
+Examples uses mostly the same coding standards as Drupal core.
+
+If you see a discrepancy between the coding standards tools used by core and
+those used by Examples, please file an issue so that Examples can follow core.
+
+Examples uses the `phpcs` tool to allow for checking PHP coding standards. We
+use the `drupal/coder` project for Drupal-specific coding standards.
+
+We also use `eslint` for JavaScript coding standards, and `csslint` for CSS.
+
+Examples has a `phpcs.xml.dist` file at the root of the project. phpcs uses this
+file to specify the current coding standards 'sniffs' which code in the project
+must pass.
+
+Contributors should install `phpcs` in their local Drupal installation, and then
+use that to run `phpcs` against Examples as part of their development and review
+process. (See details below on how to install and run this tool.)
+
+Contributors can also patch the `phpcs.xml.dist` file itself, in order to fix
+the codebase to pass a given rule or sniff. Patches which do this should be
+limited to a single rule or sniff, in order make the patch easier to review.
+
+Examples also uses the Coder project (`drupal/coder`), which adds additional
+Drupal-specific coding standards. We're currently locked to Coder version
+8.2.8, but this should change to reflect the state of core's coding standards.
+
+Installing phpcs
+----------------
+
+Current versions of Drupal 8 core require phpcs and Coder as dev dependencies.
+That means they're already probably installed in your core vendor/ directory.
+
+We need to tell `phpcs` to use the Drupal coding standard provided by Coder,
+because it isn't configured that way by default.
+
+Like this:
+
+ $ cd my/drupal/root/
+ $ ./vendor/bin/phpcs --config-set installed_paths /full/path/to/drupal/vendor/drupal/coder/coder_sniffer/
+ // phpcs now knows how to find the Drupal standard. You can test it:
+ $ cd core
+ $ ../vendor/bin/phpcs -e --standard=Drupal
+ // Shows you a bunch of Drupal-related sniffs.
+
+Running phpcs
+-------------
+
+Now you can run phpcs:
+
+ $ cd modules/examples
+ $ ../../vendor/bin/phpcs -ps
+ // phpcs uses Examples' phpcs.xml.dist to verify coding standards.
+ // -p shows you progress dots.
+ // -s shows you sniff errors in the report.
+
+If there are errors, they can sometimes be fixed with `phpcbf`, which is
+part of the `phpcs` package.
+
+ $ ../../vendor/bin/phpcbf
+ // phpcbf now performs automated fixes.
+
+Always look at the changes to see what `phpcbf` did.
+
+And always re-run `phpcs` in order to discover whether `phpcbf` handled all the
+errors.
+
+Installing eslint
+-----------------
+
+`eslint` is a node.js tool. You can and probably should install it globally,
+since installing it locally would add files to the examples project.
+Instructions available here: https://www.npmjs.com/package/eslint
+
+Examples has an `.eslintrc` file which defines the JavaScript coding standard.
+This file should be identical to the current Drupal core standard.
+
+Running eslint
+--------------
+
+You can run eslint this way:
+
+ $ cd /path/to/examples
+ $ eslint .
diff --git a/sites/all/modules/examples/examples/TESTING.md b/sites/all/modules/examples/examples/TESTING.md
new file mode 100644
index 000000000..d8398832a
--- /dev/null
+++ b/sites/all/modules/examples/examples/TESTING.md
@@ -0,0 +1,114 @@
+Testing Drupal Examples for Developers
+======================================
+
+The Drupal Examples for Developers project uses DrupalCI testing on drupal.org.
+
+That means: It runs the testbot on every patch that is marked as 'Needs Review.'
+
+Your patch might not get reviewed, and certainly won't get committed unless it
+passes the testbot.
+
+The testbot runs a script that's in your Drupal installation called
+`core/scripts/run-tests.sh`. You can run `run-tests.sh` manually and approximate
+the testbot's behavior.
+
+You can find information on how to run `run-tests.sh` locally here:
+https://www.drupal.org/node/645286
+
+Examples is always targeted to the dev branch of Drupal core for the latest
+release. As of this writing, the latest release of Drupal core is 8.2.5, which
+means development for Examples should be against the Drupal 8.2.x development
+branch. When Drupal 8.3.0 is released, we'll start targeting Examples to 8.3.x,
+and so on.
+
+You should at least run `run-tests.sh` locally against all the changes in your
+patch before uploading it.
+
+Keep in mind that unless you know you're changing behavior that is being tested
+for, the tests are not at fault. :-)
+
+Note also that, currently, using the `phpunit` tool under Drupal 8 will not find
+PHPUnit-based tests in submodules, such as phpunit_example. There is no
+suggested workaround for this, since there is no best practice to demonstrate as
+an example. There is, however, this issue in core:
+https://www.drupal.org/node/2499239
+
+How To Run The Tests In The Drupal UI
+-------------------------------------
+
+Generally, you should run tests from the command line. This is generally easier
+than using Drupal's testing UI. However, here's how you can do it that way:
+
+Enable the Testing module.
+
+Visit the test list page at `admin/config/development/testing`.
+
+Since the tests are organized by module, you can search for a module name and
+get all the tests for that module. For instance, type in 'node_type_example' for
+all the tests related to that module.
+
+Click the check boxes next to the tests you want to run. If you find this
+tedious, it's time to learn to use the command line. :-)
+
+Click 'Run Tests.' You're now running the tests.
+
+Step-by-step: How To Run The Tests.
+-----------------------------------
+
+Begin with an installed Drupal codebase. Make a codebase, set up the database,
+etc. Note that you can use an existing Drupal instance but the best practice is
+to start fresh. Something not working right? Try a new installation.
+
+Use the dev branch of core for the latest release of Drupal. As of this writing,
+it's 8.2.x. When Drupal 8.3.0 is released, we'll target 8.3.x.
+
+Open the terminal window and move to the root directory of the Drupal
+installation:
+
+ $ cd path/to/drupal
+
+Put Examples into the `modules/` folder of the Drupal installation. If you are
+doing development on Examples, you should have already checked out the git
+repository into `modules/`, like this:
+
+ $ git clone --branch 8.x-1.x https://git.drupal.org/project/examples.git modules/examples
+
+Now you can run `run-tests.sh`, which, despite having a `.sh` suffix is not a
+shell script. It's a PHP script.
+
+You'll use the `--directory` option to have the test runner scan the Examples
+module directory for tests.
+
+Also, importantly, if your test site has its own URL, you'll need to supply that
+with the `--url` option. For instance, under MAMP, you must specify
+`--url http://localhost:8888/`.
+
+You can also use `--concurrency` to speed up the test run, and `--browser` to
+see detailed test results in a web browser instead of just text output in the
+terminal.
+
+ $ php ./core/scripts/run-tests.sh --browser --concurrency 10 --url http://localhost:8888/ --directory modules/examples
+
+This should run all the tests present in Examples. If you add a test and it
+doesn't appear in the list of tests to run, then you'll need to double-check
+that it's in the proper test namespace and that the class name (and thus the
+file name) ends in Test.
+
+What Tests Should An Example Module Have?
+------------------------------------------
+
+Examples has a checklist for each module:
+https://www.drupal.org/node/2209627
+
+The reason we care about these tests is that we want the documentation
+of these APIs to be correct. If Core changes APIs, we want our tests to
+fail so that we know our documentation is incorrect.
+
+Our list of required tests includes:
+* Functional tests which verifies a 200 result for each route/path defined by
+ the module.
+* Functional tests of permission-based restrictions.
+* Functional tests which submit forms and verify that they behave as
+ expected.
+* Unit tests of unit-testable code.
+* Other. More. Better.
diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.info.yml b/sites/all/modules/examples/examples/ajax_example/ajax_example.info.yml
new file mode 100644
index 000000000..57a4145e1
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.info.yml
@@ -0,0 +1,14 @@
+name: 'AJAX Example'
+type: module
+description: 'An example module showing how to use Drupal AJAX forms.'
+package: 'Example modules'
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.libraries.yml b/sites/all/modules/examples/examples/ajax_example/ajax_example.libraries.yml
new file mode 100644
index 000000000..4df89d8c1
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.libraries.yml
@@ -0,0 +1,7 @@
+ajax_example.library:
+ version: 1.x
+ css:
+ base:
+ css/ajax-example-base.css: {}
+ js:
+ js/ajax-example.js: {}
diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.links.menu.yml b/sites/all/modules/examples/examples/ajax_example/ajax_example.links.menu.yml
new file mode 100644
index 000000000..b0c31d4ba
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.links.menu.yml
@@ -0,0 +1,72 @@
+ajax_example.description:
+ title: 'AJAX Example'
+ route_name: 'ajax_example.description'
+ expanded: TRUE
+
+ajax_example.simplest:
+ title: 'Simplest AJAX example'
+ route_name: 'ajax_example.simplest'
+ parent: ajax_example.description
+ weight: 0
+
+ajax_example.submit-driven:
+ title: 'Submit-driven AJAX'
+ route_name: 'ajax_example.submit_driven_ajax'
+ parent: ajax_example.description
+ weight: 1
+
+ajax_example.render-link:
+ title: 'AJAX link in a render array'
+ route_name: 'ajax_example.ajax_link_render'
+ parent: ajax_example.description
+ weight: 2
+
+ajax_example.wizard-example:
+ title: 'Wizard example'
+ route_name: 'ajax_example.wizard'
+ parent: ajax_example.description
+ weight: 2
+
+ajax_example.wizard-examplenojs:
+ title: 'Wizard example w/JS turned off'
+ route_name: 'ajax_example.wizardnojs'
+ parent: ajax_example.description
+ weight: 3
+
+ajax_example.autocomplete-user:
+ title: 'Autocomplete user with entity_autocomplete'
+ route_name: 'ajax_example.autocomplete_user'
+ parent: ajax_example.description
+ weight: 4
+
+ajax_example.autotextfields:
+ title: 'Generate textfields'
+ route_name: 'ajax_example.autotextfields'
+ parent: ajax_example.description
+ weight: 5
+
+ajax_example.dependent-dropdown:
+ title: 'Dependent dropdown'
+ route_name: 'ajax_example.dependent_dropdown'
+ parent: ajax_example.description
+ weight: 6
+ajax_example.dependent-dropdown-nojs:
+ title: 'Dependent dropdown w/ no JS'
+ route_name: 'ajax_example.dependent_dropdown'
+ route_parameters:
+ nojs: nojs
+ parent: ajax_example.description
+ weight: 6
+
+ajax_example.dynamic-form-sections:
+ title: 'Dynamic form sections'
+ route_name: 'ajax_example.dynamic_form_sections'
+ parent: ajax_example.description
+ weight: 10
+ajax_example.dynamic-form-sections-nojs:
+ title: 'Dynamic form sections w/ no JS'
+ route_name: 'ajax_example.dynamic_form_sections'
+ route_parameters:
+ nojs: nojs
+ parent: ajax_example.description
+ weight: 10
diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.module b/sites/all/modules/examples/examples/ajax_example/ajax_example.module
new file mode 100644
index 000000000..1c2a6de0f
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.module
@@ -0,0 +1,23 @@
+ $this->t('
+The link below has been rendered as an element with the #ajax property, so if
+javascript is enabled, ajax.js will try to submit it via an AJAX call instead
+of a normal page load. The URL also contains the "/nojs/" magic string, which
+is stripped if javascript is enabled, allowing the server code to tell by the
+URL whether JS was enabled or not, letting it do different things based on that.'),
+ ];
+ // We'll add a nice border element for our demo.
+ $build['ajax_link'] = [
+ '#type' => 'details',
+ '#title' => $this->t('This is the AJAX link'),
+ '#open' => TRUE,
+ ];
+ // We build the AJAX link.
+ $build['ajax_link']['link'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Click me'),
+ // We have to ensure that Drupal's Ajax system is loaded.
+ '#attached' => ['library' => ['core/drupal.ajax']],
+ // We add the 'use-ajax' class so that Drupal's AJAX system can spring
+ // into action.
+ '#attributes' => ['class' => ['use-ajax']],
+ // The URL for this link element is the callback. In our case, it's route
+ // ajax_example.ajax_link_callback, which maps to ajaxLinkCallback()
+ // below. The route has a /{nojs} section, which is how the callback can
+ // know whether the request was made by AJAX or some other means where
+ // JavaScript won't be able to handle the result. If the {nojs} part of
+ // the path is replaced with 'ajax', then the request was made by AJAX.
+ '#url' => Url::fromRoute('ajax_example.ajax_link_callback', ['nojs' => 'ajax']),
+ ];
+ // We provide a DIV that AJAX can append some text into.
+ $build['ajax_link']['destination'] = [
+ '#type' => 'container',
+ '#attributes' => ['id' => ['ajax-example-destination-div']],
+ ];
+ return $build;
+ }
+
+ /**
+ * Callback for link example.
+ *
+ * Takes different logic paths based on whether Javascript was enabled.
+ * If $type == 'ajax', it tells this function that ajax.js has rewritten
+ * the URL and thus we are doing an AJAX and can return an array of commands.
+ *
+ * @param string $nojs
+ * Either 'ajax' or 'nojs. Type is simply the normal URL argument to this
+ * URL.
+ *
+ * @return string|array
+ * If $type == 'ajax', returns an array of AJAX Commands.
+ * Otherwise, just returns the content, which will end up being a page.
+ */
+ public function ajaxLinkCallback($nojs = 'ajax') {
+ // Determine whether the request is coming from AJAX or not.
+ if ($nojs == 'ajax') {
+ $output = $this->t("This is some content delivered via AJAX");
+ $response = new AjaxResponse();
+ $response->addCommand(new AppendCommand('#ajax-example-destination-div', $output));
+
+ // See ajax_example_advanced.inc for more details on the available
+ // commands and how to use them.
+ // $page = array('#type' => 'ajax', '#commands' => $commands);
+ // ajax_deliver($response);
+ return $response;
+ }
+ $response = new Response($this->t("This is some content delivered via a page load."));
+ return $response;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/Autotextfields.php b/sites/all/modules/examples/examples/ajax_example/src/Form/Autotextfields.php
new file mode 100644
index 000000000..a1fa173e3
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/src/Form/Autotextfields.php
@@ -0,0 +1,124 @@
+ 'item',
+ '#markup' => $this->t('This form demonstrates changing the status of form elements through AJAX requests.'),
+ ];
+ $form['ask_first_name'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Ask me my first name'),
+ '#ajax' => [
+ 'callback' => '::textfieldsCallback',
+ 'wrapper' => 'textfields-container',
+ 'effect' => 'fade',
+ ],
+ ];
+ $form['ask_last_name'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Ask me my last name'),
+ '#ajax' => [
+ 'callback' => '::textfieldsCallback',
+ 'wrapper' => 'textfields-container',
+ 'effect' => 'fade',
+ ],
+ ];
+
+ // Wrap textfields in a container. This container will be replaced through
+ // AJAX.
+ $form['textfields_container'] = [
+ '#type' => 'container',
+ '#attributes' => ['id' => 'textfields-container'],
+ ];
+ $form['textfields_container']['textfields'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t("Generated text fields for first and last name"),
+ '#description' => t('This is where we put automatically generated textfields'),
+ ];
+
+ // This form is rebuilt on all requests, so whether or not the request comes
+ // from AJAX, we should rebuild everything based on the form state.
+ // Checkbox values are expressed as 1 or 0, so we have to be sure to compare
+ // type as well as value.
+ if ($form_state->getValue('ask_first_name', NULL) === 1) {
+ $form['textfields_container']['textfields']['first_name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('First Name'),
+ '#required' => TRUE,
+ ];
+ }
+ if ($form_state->getValue('ask_last_name', NULL) === 1) {
+ $form['textfields_container']['textfields']['last_name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Last Name'),
+ '#required' => TRUE,
+ ];
+ }
+
+ $form['textfields_container']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Click Me'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ drupal_set_message(
+ $this->t('Submit handler: First name: @first_name Last name: @last_name',
+ [
+ '@first_name' => $form_state->getValue('first_name', 'n/a'),
+ '@last_name' => $form_state->getValue('last_name', 'n/a'),
+ ]
+ )
+ );
+ }
+
+ /**
+ * Callback for ajax_example_autotextfields.
+ *
+ * Selects the piece of the form we want to use as replacement markup and
+ * returns it as a form (renderable array).
+ */
+ public function textfieldsCallback($form, FormStateInterface $form_state) {
+ return $form['textfields_container'];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/DependentDropdown.php b/sites/all/modules/examples/examples/ajax_example/src/Form/DependentDropdown.php
new file mode 100644
index 000000000..5c2e41ada
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/src/Form/DependentDropdown.php
@@ -0,0 +1,269 @@
+
+ $this->t('
Like other examples in this module, this form has a path that
+ can be modified with /nojs to simulate its behavior without JavaScript.
+
+
@try_it_without_ajax
+
@try_it_with_ajax
+
',
+ [
+ '@try_it_without_ajax' => Link::createFromRoute(
+ $this->t('Try it without AJAX'),
+ 'ajax_example.dependent_dropdown', ['nojs' => 'nojs'])
+ ->toString(),
+ '@try_it_with_ajax' => Link::createFromRoute(
+ $this->t('Try it with AJAX'),
+ 'ajax_example.dependent_dropdown')
+ ->toString(),
+ ]
+ ),
+ ];
+
+ // Our first dropdown lets us select a family of instruments: String,
+ // Woodwind, Brass, or Percussion.
+ $instrument_family_options = static::getFirstDropdownOptions();
+ // When the AJAX request occurs, this form will be build in order to process
+ // form state before the AJAX callback is called. We can use this
+ // opportunity to populate the form as we wish based on the changes to the
+ // form that caused the AJAX request. If the user caused the AJAX request,
+ // then it would have been setting a value for instrument_family_options.
+ // So if there's a value in that dropdown before we build it here, we grab
+ // it's value to help us build the specific instrument dropdown. Otherwise
+ // we can just use the value of the first item as the default value.
+ if (empty($form_state->getValue('instrument_family_dropdown'))) {
+ // Use a default value.
+ $selected_family = key($instrument_family_options);
+ }
+ else {
+ // Get the value if it already exists.
+ $selected_family = $form_state->getValue('instrument_family_dropdown');
+ }
+
+ $form['instrument_family_fieldset'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Choose an instrument family'),
+ ];
+ $form['instrument_family_fieldset']['instrument_family_dropdown'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Instrument Type'),
+ '#options' => $instrument_family_options,
+ '#default_value' => $selected_family,
+ // Bind an ajax callback to the change event (which is the default for the
+ // select form type) of the first dropdown. It will replace the second
+ // dropdown when rebuilt.
+ '#ajax' => [
+ // When 'event' occurs, Drupal will perform an ajax request in the
+ // background. Usually the default value is sufficient (eg. change for
+ // select elements), but valid values include any jQuery event,
+ // most notably 'mousedown', 'blur', and 'submit'.
+ 'callback' => '::instrumentDropdownCallback',
+ 'wrapper' => 'instrument-fieldset-container',
+ ],
+ ];
+ // Since we don't know if the user has js or not, we always need to output
+ // this element, then hide it with with css if javascript is enabled.
+ $form['instrument_family_fieldset']['choose_family'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Choose'),
+ '#attributes' => ['class' => ['ajax-example-hide', 'ajax-example-inline']],
+ ];
+ // We are using the path parameter $nojs to signal when to simulate the
+ // the user turning off JavaScript. We'll remove all the AJAX elements. This
+ // is not required, and is here so that we can demonstrate a graceful
+ // fallback without having to turn off JavaScript.
+ if ($nojs == 'nojs') {
+ // Removing the #ajax element tells the system not to use AJAX.
+ unset($form['instrument_family_fieldset']['instrument_family_dropdown']['#ajax']);
+ // Removing the ajax-example-hide class from the Choose button ensures
+ // that our JavaScript won't hide it.
+ unset($form['instrument_family_fieldset']['choose_family']['#attributes']);
+ }
+
+ // Since we're managing state for this whole fieldset (both the dropdown
+ // and enabling the Submit button), we want to replace the whole thing
+ // on AJAX requests. That's why we put it in this container.
+ $form['instrument_fieldset_container'] = [
+ '#type' => 'container',
+ '#attributes' => ['id' => 'instrument-fieldset-container'],
+ ];
+ // Build the instrument field set.
+ $form['instrument_fieldset_container']['instrument_fieldset'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Choose an instrument'),
+ ];
+ $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown'] = [
+ '#type' => 'select',
+ '#title' => $instrument_family_options[$selected_family] . ' ' . $this->t('Instruments'),
+ // When the form is rebuilt during ajax processing, the $selected_family
+ // variable will now have the new value and so the options will change.
+ '#options' => static::getSecondDropdownOptions($selected_family),
+ '#default_value' => !empty($form_state->getValue('instrument_dropdown')) ? $form_state->getValue('instrument_dropdown') : '',
+ ];
+ $form['instrument_fieldset_container']['instrument_fieldset']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+ // We might normally use #state to disable the instrument fields based on
+ // the instrument family fields. But since the premise is that we don't have
+ // JavaScript running, #state won't work either. We have to set up the state
+ // of the instrument fieldset here, based on the selected instrument family.
+ if ($selected_family == 'none') {
+ $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown']['#title'] =
+ $this->t('You must choose an instrument family.');
+ $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown']['#disabled'] = TRUE;
+ $form['instrument_fieldset_container']['instrument_fieldset']['submit']['#disabled'] = TRUE;
+ }
+ else {
+ $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown']['#disabled'] = FALSE;
+ $form['instrument_fieldset_container']['instrument_fieldset']['submit']['#disabled'] = FALSE;
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $trigger = (string) $form_state->getTriggeringElement()['#value'];
+ switch ($trigger) {
+ case 'Submit':
+ // Submit: We're done.
+ drupal_set_message($this->t('Your values have been submitted. Instrument family: @family, Instrument: @instrument', [
+ '@family' => $form_state->getValue('instrument_family_dropdown'),
+ '@instrument' => $form_state->getValue('instrument_dropdown'),
+ ]));
+ return;
+ }
+ // 'Choose' or anything else will cause rebuild of the form and present
+ // it again.
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Provide a new dropdown based on the AJAX call.
+ *
+ * This callback will occur *after* the form has been rebuilt by buildForm().
+ * Since that's the case, the form should contain the right values for
+ * instrument_dropdown.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array
+ * The portion of the render structure that will replace the
+ * instrument-dropdown-replace form element.
+ */
+ public function instrumentDropdownCallback(array $form, FormStateInterface $form_state) {
+ return $form['instrument_fieldset_container'];
+ }
+
+ /**
+ * Helper function to populate the first dropdown.
+ *
+ * This would normally be pulling data from the database.
+ *
+ * @return array
+ * Dropdown options.
+ */
+ public static function getFirstDropdownOptions() {
+ return [
+ 'none' => 'none',
+ 'String' => 'String',
+ 'Woodwind' => 'Woodwind',
+ 'Brass' => 'Brass',
+ 'Percussion' => 'Percussion',
+ ];
+ }
+
+ /**
+ * Helper function to populate the second dropdown.
+ *
+ * This would normally be pulling data from the database.
+ *
+ * @param string $key
+ * This will determine which set of options is returned.
+ *
+ * @return array
+ * Dropdown options
+ */
+ public static function getSecondDropdownOptions($key = '') {
+ switch ($key) {
+ case 'String':
+ $options = [
+ 'Violin' => 'Violin',
+ 'Viola' => 'Viola',
+ 'Cello' => 'Cello',
+ 'Double Bass' => 'Double Bass',
+ ];
+ break;
+
+ case 'Woodwind':
+ $options = [
+ 'Flute' => 'Flute',
+ 'Clarinet' => 'Clarinet',
+ 'Oboe' => 'Oboe',
+ 'Bassoon' => 'Bassoon',
+ ];
+ break;
+
+ case 'Brass':
+ $options = [
+ 'Trumpet' => 'Trumpet',
+ 'Trombone' => 'Trombone',
+ 'French Horn' => 'French Horn',
+ 'Euphonium' => 'Euphonium',
+ ];
+ break;
+
+ case 'Percussion':
+ $options = [
+ 'Bass Drum' => 'Bass Drum',
+ 'Timpani' => 'Timpani',
+ 'Snare Drum' => 'Snare Drum',
+ 'Tambourine' => 'Tambourine',
+ ];
+ break;
+
+ default:
+ $options = ['none' => 'none'];
+ break;
+ }
+ return $options;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/DynamicFormSections.php b/sites/all/modules/examples/examples/ajax_example/src/Form/DynamicFormSections.php
new file mode 100644
index 000000000..826edd703
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/src/Form/DynamicFormSections.php
@@ -0,0 +1,222 @@
+
+ $this->t('
Like other examples in this module, this form has a path that
+ can be modified with /nojs to simulate its behavior without JavaScript.
+
+
@try_it_without_ajax
+
@try_it_with_ajax
+
',
+ [
+ '@try_it_without_ajax' => Link::createFromRoute(
+ $this->t('Try it without AJAX'),
+ 'ajax_example.dynamic_form_sections', ['nojs' => 'nojs'])
+ ->toString(),
+ '@try_it_with_ajax' => Link::createFromRoute(
+ $this->t('Try it with AJAX'),
+ 'ajax_example.dynamic_form_sections')
+ ->toString(),
+ ]
+ ),
+ ];
+
+ $form['question_type_select'] = [
+ // This is our select dropdown.
+ '#type' => 'select',
+ '#title' => t('Question style'),
+ // We have a variety of form items you can use to get input from the user.
+ '#options' => [
+ 'Choose question style' => 'Choose question style',
+ 'Multiple Choice' => 'Multiple Choice',
+ 'True/False' => 'True/False',
+ 'Fill-in-the-blanks' => 'Fill-in-the-blanks',
+ ],
+ // The #ajax section tells the AJAX system that whenever this dropdown
+ // emits an event, it should call the callback and put the resulting
+ // content into the wrapper we specify. The questions-fieldset-wrapper is
+ // defined below.
+ '#ajax' => [
+ 'wrapper' => 'questions-fieldset-wrapper',
+ 'callback' => '::promptCallback',
+ ],
+ ];
+ // The CSS for this module hides this next button if JS is enabled.
+ $form['question_type_submit'] = [
+ '#type' => 'submit',
+ '#value' => t('Choose'),
+ '#attributes' => ['class' => ['ajax-example-inline']],
+ // No need to validate when submitting this.
+ '#limit_validation_errors' => [],
+ '#validate' => [],
+ ];
+
+ // This section allows us to demonstrate no-AJAX use without turning off
+ // javascript in the browser.
+ if ($nojs != 'nojs') {
+ // Allow JavaScript to hide the choose button if we're using AJAX.
+ $form['question_type_submit']['#attributes']['class'][] = 'ajax-example-hide';
+ }
+ else {
+ // Remove #ajax from the above, so it won't perform AJAX behaviors.
+ unset($form['question_type_select']['#ajax']);
+ }
+
+ // This fieldset just serves as a container for the part of the form
+ // that gets rebuilt. It has a nice line around it so you can see it.
+ $form['questions_fieldset'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Stuff will appear here'),
+ '#open' => TRUE,
+ // We set the ID of this fieldset to questions-fieldset-wrapper so the
+ // AJAX command can replace it.
+ '#attributes' => ['id' => 'questions-fieldset-wrapper'],
+ ];
+
+ // When the AJAX request comes in, or when the user hit 'Submit' if there is
+ // no JavaScript, the form state will tell us what the user has selected
+ // from the dropdown. We can look at the value of the dropdown to determine
+ // which secondary form to display.
+ $question_type = $form_state->getValue('question_type_select');
+ if (!empty($question_type) && $question_type !== 'Choose question style') {
+
+ $form['questions_fieldset']['question'] = [
+ '#markup' => t('Who was the first president of the U.S.?'),
+ ];
+
+ // Build up a secondary form, based on the type of question the user
+ // chose.
+ switch ($question_type) {
+ case 'Multiple Choice':
+ $form['questions_fieldset']['question'] = [
+ '#type' => 'radios',
+ '#title' => t('Who was the first president of the United States'),
+ '#options' => [
+ 'George Bush' => 'George Bush',
+ 'Adam McGuire' => 'Adam McGuire',
+ 'Abraham Lincoln' => 'Abraham Lincoln',
+ 'George Washington' => 'George Washington',
+ ],
+
+ ];
+ break;
+
+ case 'True/False':
+ $form['questions_fieldset']['question'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Was George Washington the first president of the United States?'),
+ '#options' => [
+ 'George Washington' => 'True',
+ 0 => 'False',
+ ],
+ '#description' => $this->t('Click "True" if you think George Washington was the first president of the United States.'),
+ ];
+ break;
+
+ case 'Fill-in-the-blanks':
+ $form['questions_fieldset']['question'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Who was the first president of the United States'),
+ '#description' => $this->t('Please type the correct answer to the question.'),
+ ];
+ break;
+ }
+
+ $form['questions_fieldset']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit your answer'),
+ ];
+ }
+ return $form;
+ }
+
+ /**
+ * Final submit handler.
+ *
+ * Reports what values were finally set.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // This is only executed when a button is pressed, not when the AJAXfield
+ // select is changed.
+ // Now handle the case of the next, previous, and submit buttons.
+ // Only submit will result in actual submission, all others rebuild.
+ if ($form_state->getValue('question_type_submit') == 'Choose') {
+ $form_state->setValue('question_type_select', $form_state->getUserInput()['question_type_select']);
+ $form_state->setRebuild();
+ }
+
+ if ($form_state->getValue('submit') == 'Submit your answer') {
+ $form_state->setRebuild(FALSE);
+ $answer = $form_state->getValue('question');
+ // Special handling for the checkbox.
+ if ($answer == 1 && $form['questions_fieldset']['question']['#type'] == 'checkbox') {
+ $answer = $form['questions_fieldset']['question']['#title'];
+ }
+ if ($answer == $this->t('George Washington')) {
+ drupal_set_message($this->t('You got the right answer: @answer', ['@answer' => $answer]));
+ }
+ else {
+ drupal_set_message($this->t('Sorry, your answer (@answer) is wrong', ['@answer' => $answer]));
+ }
+ return;
+ }
+ // Sets the form to be rebuilt after processing.
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Callback for the select element.
+ *
+ * Since the questions_fieldset part of the form has already been built during
+ * the AJAX request, we can return only that part of the form to the AJAX
+ * request, and it will insert that part into questions-fieldset-wrapper.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array
+ * The form structure.
+ */
+ public function promptCallback(array $form, FormStateInterface $form_state) {
+ return $form['questions_fieldset'];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/EntityAutocomplete.php b/sites/all/modules/examples/examples/ajax_example/src/Form/EntityAutocomplete.php
new file mode 100644
index 000000000..53b0162cd
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/src/Form/EntityAutocomplete.php
@@ -0,0 +1,126 @@
+get('entity_type.manager')
+ );
+ $form->setStringTranslation($container->get('string_translation'));
+ return $form;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager service.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'ajax_example_autocomplete_user';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['info'] = [
+ '#markup' => '
' . t("This example uses the entity_autocomplete form "
+ . "element to select users. You'll need a few users on your system for "
+ . "it to make sense.") . '
',
+ ];
+
+ // Here we use the delightful entity_autocomplete form element. It allows us
+ // to consistently select entities. See https://www.drupal.org/node/2418529.
+ $form['users'] = [
+ // A type of entity_autocomplete lets Drupal know it should autocomplete
+ // entities.
+ '#type' => 'entity_autocomplete',
+ // We can specify entity types to autocomplete.
+ '#target_type' => 'user',
+ // Specifying #tags as TRUE allows for multiple selections, separated by
+ // commas.
+ '#tags' => TRUE,
+ '#title' => t('Choose a user. Separate with commas.'),
+ ];
+
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Here we validate and signal an error if there are no users selected.
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $state_users = $form_state->getValue('users');
+ if (empty($state_users)) {
+ $form_state->setErrorByName('users', 'There were no users selected.');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * On submit, show the user the names of the users they selected.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $state_users = $form_state->getValue('users');
+ $users = [];
+ foreach ($state_users as $state_user) {
+ $uid = $state_user['target_id'];
+ $users[] = $this->entityTypeManager->getStorage('user')->load($uid)->getUsername();
+ }
+ drupal_set_message('These are your users: ' . implode(' ', $users));
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/Simplest.php b/sites/all/modules/examples/examples/ajax_example/src/Form/Simplest.php
new file mode 100644
index 000000000..951a7f9cc
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/src/Form/Simplest.php
@@ -0,0 +1,80 @@
+ $this->t("Choose something and explain why"),
+ '#type' => 'select',
+ '#options' => [
+ 'one' => 'one',
+ 'two' => 'two',
+ 'three' => 'three',
+ ],
+ '#ajax' => [
+ // #ajax has two required keys: callback and wrapper.
+ // 'callback' is a function that will be called when this element
+ // changes.
+ 'callback' => '::promptCallback',
+ // 'wrapper' is the HTML id of the page element that will be replaced.
+ 'wrapper' => 'replace-textfield-container',
+ ],
+ ];
+
+ // The 'replace-textfield-container' container will be replaced whenever
+ // 'changethis' is updated.
+ $form['replace_textfield_container'] = [
+ '#type' => 'container',
+ '#attributes' => ['id' => 'replace-textfield-container'],
+ ];
+ $form['replace_textfield_container']['replace_textfield'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t("Why"),
+ ];
+
+ // An AJAX request calls the form builder function for every change.
+ // We can change how we build the form based on $form_state.
+ $value = $form_state->getValue('changethis');
+ // The getValue() method returns NULL by default if the form element does
+ // not exist. It won't exist yet if we're building it for the first time.
+ if ($value !== NULL) {
+ $form['replace_textfield_container']['replace_textfield']['#description'] =
+ $this->t("Say why you chose '@value'", ['@value' => $value]);
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // No-op. Our form doesn't need a submit handler, because the form is never
+ // submitted. We add the method here so we fulfill FormInterface.
+ }
+
+ /**
+ * Handles switching the available regions based on the selected theme.
+ */
+ public function promptCallback($form, FormStateInterface $form_state) {
+ return $form['replace_textfield_container'];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/SubmitDriven.php b/sites/all/modules/examples/examples/ajax_example/src/Form/SubmitDriven.php
new file mode 100644
index 000000000..8e99f134d
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/src/Form/SubmitDriven.php
@@ -0,0 +1,73 @@
+ 'container',
+ '#attributes' => ['id' => 'box-container'],
+ ];
+ // The box contains some markup that we can change on a submit request.
+ $form['container']['box'] = [
+ '#type' => 'markup',
+ '#markup' => '
Initial markup for box
',
+ ];
+
+ $form['submit'] = [
+ '#type' => 'submit',
+ // The AJAX handler will call our callback, and will replace whatever page
+ // element has id box-container.
+ '#ajax' => [
+ 'callback' => '::promptCallback',
+ 'wrapper' => 'box-container',
+ ],
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ }
+
+ /**
+ * Callback for submit_driven example.
+ *
+ * Select the 'box' element, change the markup in it, and return it as a
+ * renderable array.
+ *
+ * @return array
+ * Renderable array (the box element)
+ */
+ public function promptCallback(array &$form, FormStateInterface $form_state) {
+ // In most cases, it is recommended that you put this logic in form
+ // generation rather than the callback. Submit driven forms are an
+ // exception, because you may not want to return the form at all.
+ $element = $form['container'];
+ $element['box']['#markup'] = "Clicked submit ({$form_state->getValue('op')}): " . date('c');
+ return $element;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/Wizard.php b/sites/all/modules/examples/examples/ajax_example/src/Form/Wizard.php
new file mode 100644
index 000000000..acf1f68e7
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/src/Form/Wizard.php
@@ -0,0 +1,229 @@
+t('examples/ajax-example/wizard-nojs'), $url)
+ ->toString();
+
+ // Prepare link for multiple arguments.
+ $urltwo = Url::fromUri('internal:/examples/ajax-example/wizard');
+ $linktwo = Link::fromTextAndUrl($this->t('examples/ajax-example/wizard'), $urltwo)
+ ->toString();
+
+ // We want to deal with hierarchical form values.
+ $form['#tree'] = TRUE;
+ $form['description'] = [
+ '#markup' => t('This example is a step-by-step wizard. The @link does it without page reloads; the @link1 is the same code but simulates a non-javascript environment, showing it with page reloads.', [
+ '@link' => $linktwo,
+ '@link1' => $link,
+ ]),
+ ];
+
+ $form['step'] = [
+ '#type' => 'hidden',
+ '#value' => !empty($form_state->getValue('step')) ? $form_state->getValue('step') : 1,
+ ];
+ print_r($form_state->getValue('step'));
+
+ if ($form['step']['#value'] == 1) {
+ $form['step1'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Step 1: Personal details'),
+ ];
+ $form['step1']['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Your name'),
+ '#default_value' => empty($form_state->getValue([
+ 'step1',
+ 'name',
+ ]) ? '' : $form_state->getValue(['step1', 'name'])),
+ '#required' => TRUE,
+ ];
+
+ $form['next'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Next step'),
+ '#ajax' => [
+ 'wrapper' => 'ajax-example-wizard',
+ 'callback' => '::prompt',
+ ],
+ ];
+ }
+
+ // This simply allows us to demonstrate no-javascript use without
+ // actually turning off javascript in the browser. Removing the #ajax
+ // element turns off AJAX behaviors on that element and as a result
+ // ajax.js doesn't get loaded.
+ // For demonstration only! You don't need this.
+ if ($no_js_use) {
+ // Remove the #ajax from the above, so ajax.js won't be loaded.
+ // For demonstration only.
+ unset($form['next']['#ajax']);
+ unset($form['prev']['#ajax']);
+ }
+
+ return $form;
+ }
+
+ /**
+ * Wizard callback function.
+ *
+ * @param array $form
+ * Form API form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form API form.
+ *
+ * @return array
+ * Form array.
+ */
+ public function prompt(array $form, FormStateInterface $form_state) {
+ return $form;
+ }
+
+ /**
+ * Save away the current information.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ if ($form_state->getTriggeringElement()['#value'] == $this->t('Submit your information')) {
+ $value_message = $this->t('Your information has been submitted:') . ' ';
+ foreach ($form_state->getValue('value') as $step => $values) {
+ $value_message .= "$step: ";
+ foreach ($values as $key => $value) {
+ $value_message .= "$key=$value, ";
+ }
+ }
+ drupal_set_message($value_message);
+ $form_state->setRebuild(FALSE);
+ // Redirect to #action, else return.
+ return;
+ }
+ else {
+ $step = $form_state->getValue('step');
+ // Increment or decrement the step as needed. Recover values if they
+ // exist.
+ if ($form_state->getTriggeringElement()['#value']->__toString() == $this->t('Next step')) {
+ $step++;
+ }
+ elseif ($form_state->getTriggeringElement()['#value']->__toString() == $this->t('Previous step')) {
+ $step--;
+ }
+
+ switch ($step) {
+ case 1:
+ $form['step1'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Step 1: Personal details'),
+ ];
+ $form['step1']['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Your name'),
+ '#default_value' => empty($form_state->getValue([
+ 'step1',
+ 'name',
+ ]) ? '' : $form_state->getValue(['step1', 'name'])),
+ '#required' => TRUE,
+ ];
+ $form_state->setValue('step', 1);
+ break;
+
+ case 2:
+ unset($form['step1']);
+ unset($form['next']);
+ $form['step2'] = [
+ '#type' => 'fieldset',
+ '#title' => t('Step 2: Street address info'),
+ ];
+ $form['step2']['address'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Your street address'),
+ '#default_value' => empty($form_state->getValue([
+ 'step2',
+ 'address',
+ ]) ? '' : $form_state->getValue(['step2', 'address'])),
+ '#required' => TRUE,
+ ];
+ $form_state->setValue('step', $step);
+ break;
+
+ case 3:
+
+ $form['step3'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Step 3: City info'),
+ ];
+ $form['step3']['city'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Your city'),
+ '#default_value' => empty($form_state->getValue([
+ 'step3',
+ 'city',
+ ]) ? '' : $form_state->getValue(['step3', 'city'])),
+ '#required' => TRUE,
+ ];
+ $form_state->setValue('step', $step);
+ break;
+ }
+ if ($step == 3) {
+
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t("Submit your information"),
+ ];
+ }
+ if ($step > 1 && !isset($form['prev'])) {
+ $form['prev'] = [
+ '#type' => 'submit',
+ '#value' => t("Previous step"),
+ // Since all info will be discarded, don't validate on 'prev'.
+ '#limit_validation_errors' => [],
+ // #submit is required to use #limit_validation_errors.
+ '#submit' => ['ajax_example_wizard_submit'],
+ '#ajax' => [
+ 'wrapper' => 'ajax-example-wizard',
+ 'callback' => '::prompt',
+ ],
+ ];
+ }
+ if ($step < 3 && !isset($form['next'])) {
+ $form['next'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Next step'),
+ '#limit_validation_errors' => [],
+ '#ajax' => [
+ 'wrapper' => 'ajax-example-wizard',
+ 'callback' => '::prompt',
+ ],
+ ];
+ }
+ $response = new AjaxResponse();
+ $response->addCommand(new HtmlCommand('#ajax-example-wizard', $form));
+ return $response;
+ }
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/templates/description.html.twig b/sites/all/modules/examples/examples/ajax_example/templates/description.html.twig
new file mode 100644
index 000000000..76ed8e5f8
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/templates/description.html.twig
@@ -0,0 +1,29 @@
+{#
+
+Description text for the Ajax Example.
+
+#}
+
+{% set simple_ajax_example = path('ajax_example.simplest') %}
+{% set ajax_generate_textfields = path('ajax_example.autotextfields') %}
+{% set ajax_submit = path('ajax_example.submit_driven_ajax') %}
+{% set ajax_dependent_dropdown = path('ajax_example.dependent_dropdown') %}
+{% set ajax_dependent_dropdown_nojs = path('ajax_example.dependent_dropdown', {'nojs': 'nojs'}) %}
+{% set ajax_dynamic_form = path('ajax_example.dynamic_form_sections') %}
+{% set ajax_dynamic_form_nojs = path('ajax_example.dynamic_form_sections', {'nojs': 'nojs'}) %}
+{% set ajax_wizard_example = path('ajax_example.wizard') %}
+{% set ajax_wizard_example_nojs = path('ajax_example.wizardnojs') %}
+
+{% trans %}
+
+
The AJAX example module provides many examples of AJAX including forms, links, and AJAX commands.
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php
new file mode 100644
index 000000000..4528cc411
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php
@@ -0,0 +1,79 @@
+drupalLogin(
+ $this->createUser(['access content', 'access user profiles'])
+ );
+
+ $assertion = $this->assertSession();
+
+ // Routes with menu links, and their form buttons.
+ $routes_with_menu_links = [
+ 'ajax_example.description' => [],
+ 'ajax_example.simplest' => [],
+ 'ajax_example.autotextfields' => ['Click Me'],
+ 'ajax_example.submit_driven_ajax' => ['Submit'],
+ 'ajax_example.dependent_dropdown' => ['Submit'],
+ 'ajax_example.dynamic_form_sections' => ['Choose'],
+ 'ajax_example.wizard' => ['Next step'],
+ 'ajax_example.wizardnojs' => ['Next step'],
+ 'ajax_example.ajax_link_render' => [],
+ 'ajax_example.autocomplete_user' => ['Submit'],
+ ];
+
+ // Ensure the links appear in the tools menu sidebar.
+ $this->drupalGet('');
+ foreach (array_keys($routes_with_menu_links) as $route) {
+ $assertion->linkByHrefExists(Url::fromRoute($route)->getInternalPath());
+ }
+
+ // All our routes with their form buttons.
+ $routes = [
+ 'ajax_example.ajax_link_callback' => [],
+ ];
+
+ // Go to all the routes and click all the buttons.
+ $routes = array_merge($routes_with_menu_links, $routes);
+ foreach ($routes as $route => $buttons) {
+ $url = Url::fromRoute($route);
+ $this->drupalGet($url);
+ $assertion->statusCodeEquals(200);
+ foreach ($buttons as $button) {
+ $this->drupalPostForm($url, [], $button);
+ $assertion->statusCodeEquals(200);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DependentDropdownTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DependentDropdownTest.php
new file mode 100644
index 000000000..11c131d34
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DependentDropdownTest.php
@@ -0,0 +1,74 @@
+getSession();
+ $assert = $this->assertSession();
+ $page = $session->getPage();
+
+ // Get a URL object for the form, specifying no JS.
+ $dropdown_url = Url::fromRoute('ajax_example.dependent_dropdown', ['nojs' => 'nojs']);
+
+ // Get the form.
+ $this->drupalGet($dropdown_url);
+ // Check for the initial state.
+ $assert->fieldDisabled('instrument_dropdown');
+ $assert->fieldValueEquals('instrument_dropdown', 'none');
+ $submit_button = $page->findButton('edit-submit');
+ $this->assertTrue($submit_button->hasAttribute('disabled'));
+
+ // Run through the matrix of form submissions.
+ $families = [
+ 'String' => ['Violin', 'Viola', 'Cello', 'Double Bass'],
+ 'Woodwind' => ['Flute', 'Clarinet', 'Oboe', 'Bassoon'],
+ 'Brass' => ['Trumpet', 'Trombone', 'French Horn', 'Euphonium'],
+ 'Percussion' => ['Bass Drum', 'Timpani', 'Snare Drum', 'Tambourine'],
+ ];
+
+ foreach ($families as $family => $instruments) {
+ // Post the form for the instrument family.
+ $this->drupalPostForm($dropdown_url, ['instrument_family_dropdown' => $family], 'Choose');
+ // Get the instrument dropdown elements.
+ $instrument_options = $page->findAll('css', '#edit-instrument-dropdown option');
+ $this->assertCount(count($instruments), $instrument_options);
+ // Make sure all the instruments are in the select dropdown.
+ foreach ($instrument_options as $instrument) {
+ $this->assertContains($instrument->getAttribute('value'), $instruments);
+ }
+ // Post each instrument. We have to 'choose' again in order to unlock the
+ // instrument dropdown.
+ foreach ($instruments as $instrument) {
+ $this->drupalPostForm($dropdown_url, ['instrument_family_dropdown' => $family], 'Choose');
+ $this->drupalPostForm(NULL, ['instrument_dropdown' => $instrument], 'Submit');
+ $assert->pageTextContains("Your values have been submitted. Instrument family: $family, Instrument: $instrument");
+ }
+ }
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DynamicFormSectionsTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DynamicFormSectionsTest.php
new file mode 100644
index 000000000..6403aa091
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DynamicFormSectionsTest.php
@@ -0,0 +1,70 @@
+assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Get a URL object for the form, specifying no JS.
+ $dropdown_url = Url::fromRoute('ajax_example.dynamic_form_sections', ['nojs' => 'nojs']);
+
+ // Get the form.
+ $this->drupalGet($dropdown_url);
+ // Check for the initial state.
+ $detail_children = $page->findAll('css', 'div.details-wrapper *');
+ $this->assertEmpty($detail_children);
+
+ // Go through the dropdown options. First outlier is 'Choose question style'
+ // which should have an empty details section.
+ $this->drupalPostForm($dropdown_url, ['question_type_select' => 'Choose question style'], 'Choose');
+ $detail_children = $page->findAll('css', 'div.details-wrapper *');
+ $this->assertEmpty($detail_children);
+
+ // Cycle through the other dropdown values.
+ $question_styles = [
+ 'Multiple Choice',
+ 'True/False',
+ 'Fill-in-the-blanks',
+ ];
+ // These all add stuff to the details wrapper.
+ foreach ($question_styles as $question_style) {
+ $this->drupalPostForm($dropdown_url, ['question_type_select' => $question_style], 'Choose');
+ $detail_children = $page->findAll('css', 'div.details-wrapper *');
+ $this->assertNotEmpty($detail_children);
+ $this->drupalPostForm(NULL, ['question' => 'George Washington'], 'Submit your answer');
+ $assert->pageTextContains('You got the right answer: George Washington');
+ }
+ // One wrong answer to exercise that code path.
+ $this->drupalPostForm($dropdown_url, ['question_type_select' => 'Multiple Choice'], 'Choose');
+ $detail_children = $page->findAll('css', 'div.details-wrapper *');
+ $this->assertNotEmpty($detail_children);
+ $this->drupalPostForm(NULL, ['question' => 'Abraham Lincoln'], 'Submit your answer');
+ $assert->pageTextContains('Sorry, your answer (Abraham Lincoln) is wrong');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/AutotextfieldsTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/AutotextfieldsTest.php
new file mode 100644
index 000000000..44e82ab43
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/AutotextfieldsTest.php
@@ -0,0 +1,80 @@
+getSession();
+ $page = $session->getPage();
+ $assert = $this->assertSession();
+
+ // Get the page.
+ $form_url = Url::fromRoute('ajax_example.autotextfields');
+ $this->drupalGet($form_url);
+
+ // Check our initial state.
+ $assert->checkboxNotChecked('ask_first_name');
+ $assert->checkboxNotChecked('ask_last_name');
+ $assert->fieldNotExists('first_name');
+ $assert->fieldNotExists('last_name');
+ // Submit the form. This tests what happens when there are no user
+ // interactions because drupalPostForm() reloads the form.
+ $this->drupalPostForm($form_url, [], 'Click Me');
+ $assert->pageTextContains('Submit handler: First name: n/a Last name: n/a');
+
+ // Ask for the first name.
+ $page->checkField('ask_first_name');
+ $assert->assertWaitOnAjaxRequest();
+ $assert->fieldExists('first_name');
+ $assert->fieldNotExists('last_name');
+ // Submit the form. We have to find the field and set its value rather than
+ // use drupalPostForm(), because when we post the form, it will be rebuilt.
+ // We are testing the form state after AJAX has modified it, so we must
+ // preserve that.
+ $page->findField('first_name')->setValue('Dries');
+ $page->pressButton('Click Me');
+ $assert->pageTextContains('Submit handler: First name: Dries Last name: n/a');
+
+ // Ask for the first and last name.
+ $page->checkField('ask_first_name');
+ $assert->assertWaitOnAjaxRequest();
+ $assert->fieldExists('first_name');
+ $page->checkField('ask_last_name');
+ $assert->assertWaitOnAjaxRequest();
+ $assert->fieldExists('last_name');
+ // Submit the form.
+ $page->findField('first_name')->setValue('Dries');
+ $page->findField('last_name')->setValue('Buytaert');
+ $page->pressButton('Click Me');
+ $assert->pageTextContains('Submit handler: First name: Dries Last name: Buytaert');
+
+ // Ask for only the last name.
+ $page->checkField('ask_last_name');
+ $assert->assertWaitOnAjaxRequest();
+ $assert->fieldNotExists('first_name');
+ $assert->fieldExists('last_name');
+ // Submit the form.
+ $page->findField('last_name')->setValue('Buytaert');
+ $page->pressButton('Click Me');
+ $assert->pageTextContains('Submit handler: First name: n/a Last name: Buytaert');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DependentDropdownTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DependentDropdownTest.php
new file mode 100644
index 000000000..8ab642254
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DependentDropdownTest.php
@@ -0,0 +1,75 @@
+assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Get a URL object for the form, specifying AJAX.
+ $dropdown_url = Url::fromRoute('ajax_example.dependent_dropdown', ['nojs' => 'ajax']);
+
+ // Get the form.
+ $this->drupalGet($dropdown_url);
+ // Check for the initial state.
+ $assert->fieldDisabled('instrument_dropdown');
+ $assert->fieldValueEquals('instrument_dropdown', 'none');
+ $submit_button = $page->findButton('edit-submit');
+ $this->assertTrue($submit_button->hasAttribute('disabled'));
+
+ // Run through the matrix of families.
+ $families = [
+ 'String' => ['Violin', 'Viola', 'Cello', 'Double Bass'],
+ 'Woodwind' => ['Flute', 'Clarinet', 'Oboe', 'Bassoon'],
+ 'Brass' => ['Trumpet', 'Trombone', 'French Horn', 'Euphonium'],
+ 'Percussion' => ['Bass Drum', 'Timpani', 'Snare Drum', 'Tambourine'],
+ ];
+
+ foreach ($families as $family => $instruments) {
+ // Select a family.
+ $family_dropdown = $assert->fieldExists('instrument_family_dropdown');
+ $family_dropdown->setValue($family);
+ $assert->assertWaitOnAjaxRequest();
+
+ // Get the instrument dropdown elements.
+ $instrument_options = $page->findAll('css', 'select[name="instrument_dropdown"] option');
+ $this->assertCount(count($instruments), $instrument_options);
+ // Make sure all the instruments are in the select dropdown.
+ foreach ($instrument_options as $instrument) {
+ $this->assertContains($instrument->getAttribute('value'), $instruments);
+ }
+
+ // Post each instrument.
+ foreach ($instruments as $instrument) {
+ $this->drupalGet($dropdown_url);
+ $family_dropdown->setValue($family);
+ $assert->assertWaitOnAjaxRequest();
+ $this->drupalPostForm(NULL, ['instrument_dropdown' => $instrument], 'Submit');
+ $assert->pageTextContains("Your values have been submitted. Instrument family: $family, Instrument: $instrument");
+ }
+ }
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DynamicFormSectionsTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DynamicFormSectionsTest.php
new file mode 100644
index 000000000..539e34ec3
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DynamicFormSectionsTest.php
@@ -0,0 +1,67 @@
+assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Get a URL object for the form, specifying no JS.
+ $dropdown_url = Url::fromRoute('ajax_example.dynamic_form_sections', ['nojs' => 'ajax']);
+
+ // Get the form.
+ $this->drupalGet($dropdown_url);
+ // Check for the initial state.
+ $this->assertEmpty($page->findAll('css', 'div.details-wrapper *'));
+
+ // Cycle through the other dropdown values.
+ $question_styles = [
+ 'Multiple Choice',
+ 'True/False',
+ 'Fill-in-the-blanks',
+ ];
+
+ // Check expectations against the details wrapper.
+ $question_type_dropdown = $page->findField('question_type_select');
+ foreach ($question_styles as $question_style) {
+ $question_type_dropdown->setValue($question_style);
+ $assert->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($page->findAll('css', 'div.details-wrapper *'));
+ }
+ // Prompt to choose question should remove the question.
+ $question_type_dropdown->setValue('Choose question style');
+ $assert->assertWaitOnAjaxRequest();
+ $this->assertEmpty($page->findAll('css', 'div.details-wrapper *'));
+
+ // Submit the correct answers.
+ foreach ($question_styles as $question_style) {
+ $this->drupalGet($dropdown_url);
+ $question_type_dropdown->setValue($question_style);
+ $assert->assertWaitOnAjaxRequest();
+ $this->drupalPostForm(NULL, ['question' => 'George Washington'], 'Submit your answer');
+ $assert->pageTextContains('You got the right answer: George Washington');
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php
new file mode 100644
index 000000000..e1588dc76
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php
@@ -0,0 +1,69 @@
+createUser([], $name);
+ }
+
+ // Get our various Mink elements.
+ $assert = $this->assertSession();
+ $session = $this->getSession();
+ $page = $session->getPage();
+ // We'll be using the users field quite a bit, so let's make it a variable.
+ $users_field_name = 'edit-users';
+
+ // Get the form.
+ $this->drupalGet(Url::fromRoute('ajax_example.autocomplete_user'));
+ // Examine the DOM to make sure our change hasn't happened yet.
+ $assert->fieldValueEquals($users_field_name, '');
+
+ // Send an event to the DOM. This will start the autocomplete process.
+ $autocomplete_field = $page->findField($users_field_name);
+ $session->getDriver()->keyDown($autocomplete_field->getXpath(), 'b');
+
+ // Wait for the autocomplete request to complete.
+ $assert->waitOnAutocomplete();
+
+ // Examine the DOM to see if our expected change happened.
+ $results = $page->findAll('css', '.ui-autocomplete li');
+ $this->assertCount(2, $results);
+ foreach ($results as $result) {
+ $this->assertContains($result->getText(), $names);
+ }
+
+ // Submit to see if our form processed the user properly.
+ $this->submitForm([$users_field_name => 'bb, bc'], 'Submit');
+ $assert->pageTextContains('These are your users: bb bc');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SimplestTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SimplestTest.php
new file mode 100644
index 000000000..243b5ea55
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SimplestTest.php
@@ -0,0 +1,60 @@
+getSession();
+ $page = $session->getPage();
+ $assert = $this->assertSession();
+
+ // Get the page.
+ $form_url = Url::fromRoute('ajax_example.simplest');
+ $this->drupalGet($form_url);
+
+ // Don't repeat ourselves. This makes it easier if we change the markup
+ // later.
+ $description_selector = '#replace-textfield-container div.description';
+
+ // Check our initial state.
+ $assert->elementExists('css', '#replace-textfield-container');
+ $assert->elementNotExists('css', $description_selector);
+
+ // Select values on the dropdown. Start with three so the change event is
+ // triggered.
+ foreach (['three', 'two', 'one'] as $value) {
+ // Select the dropdown value.
+ $page->selectFieldOption('changethis', $value);
+ // Wait for AJAX to happen.
+ $assert->assertWaitOnAjaxRequest();
+ // Assert that the description exists.
+ $assert->elementExists('css', $description_selector);
+ // Get the description element from the page.
+ $prompt_element = $page->find('css', $description_selector);
+ // Assert that the description element says what we expect it to say.
+ $this->assertEquals(
+ "Say why you chose '$value'",
+ $prompt_element->getText()
+ );
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php
new file mode 100644
index 000000000..e13d1997c
--- /dev/null
+++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php
@@ -0,0 +1,45 @@
+assertSession();
+ // Get the page.
+ $this->drupalGet(Url::fromRoute('ajax_example.submit_driven_ajax'));
+ // Examine the DOM to make sure our change hasn't happened yet.
+ $assert->pageTextNotContains('Clicked submit (Submit):');
+ // Submit the form.
+ $this->submitForm([], 'Submit');
+ // Wait on the AJAX request.
+ $assert->assertWaitOnAjaxRequest();
+ // Compare DOM to our expectations.
+ $assert->pageTextContains('Clicked submit (Submit):');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.info.yml b/sites/all/modules/examples/examples/batch_example/batch_example.info.yml
new file mode 100644
index 000000000..25e8adc7e
--- /dev/null
+++ b/sites/all/modules/examples/examples/batch_example/batch_example.info.yml
@@ -0,0 +1,14 @@
+name: Batch Example
+type: module
+description: An example outlining how a module can define batch operations.
+package: Example modules
+# core: 8.x
+dependencies:
+ - examples
+ - toolbar
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.install b/sites/all/modules/examples/examples/batch_example/batch_example.install
new file mode 100644
index 000000000..857b4d767
--- /dev/null
+++ b/sites/all/modules/examples/examples/batch_example/batch_example.install
@@ -0,0 +1,83 @@
+fetchField();
+ // A place to store messages during the run.
+ $sandbox['messages'] = [];
+ // Last node read via the query.
+ $sandbox['current_node'] = -1;
+ }
+
+ // Process nodes by groups of 10 (arbitrary value).
+ // When a group is processed, the batch update engine determines
+ // whether it should continue processing in the same request or provide
+ // progress feedback to the user and wait for the next request.
+ $limit = 10;
+
+ // Retrieve the next group of nids.
+ $query = db_select('node', 'n');
+ $query->fields('n', ['nid']);
+ $result = $query
+ ->where('n.nid > :nid', [':nid' => $sandbox['current_node']])
+ ->range(0, $limit)
+ ->orderBy('n.nid', 'ASC')
+ ->execute();
+ foreach ($result as $row) {
+ // Here we actually perform a dummy 'update' on the current node.
+ $node = db_query('SELECT nid FROM {node} WHERE nid = :nid', [':nid' => $row->nid])->fetchField();
+
+ // Update our progress information.
+ $sandbox['progress']++;
+ $sandbox['current_node'] = $row->nid;
+ }
+
+ // Set the "finished" status, to tell batch engine whether this function
+ // needs to run again. If you set a float, this will indicate the progress
+ // of the batch so the progress bar will update.
+ $sandbox['#finished'] = ($sandbox['progress'] >= $sandbox['max']) ? TRUE : ($sandbox['progress'] / $sandbox['max']);
+
+ // Set up a per-run message; Make a copy of $sandbox so we can change it.
+ // This is simply a debugging stanza to illustrate how to capture status
+ // from each pass through hook_update_N().
+ $sandbox_status = $sandbox;
+ // Don't want them in the output.
+ unset($sandbox_status['messages']);
+ $sandbox['messages'][] = t('$sandbox=') . print_r($sandbox_status, TRUE);
+
+ if ($sandbox['#finished']) {
+ // hook_update_N() may optionally return a string which will be displayed
+ // to the user.
+ $final_message = '
' . implode('
', $sandbox['messages']) . "
";
+ return t('The batch_example demonstration update did what it was supposed to do: @message', ['@message' => $final_message]);
+ }
+}
diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.links.menu.yml b/sites/all/modules/examples/examples/batch_example/batch_example.links.menu.yml
new file mode 100644
index 000000000..272cebbf8
--- /dev/null
+++ b/sites/all/modules/examples/examples/batch_example/batch_example.links.menu.yml
@@ -0,0 +1,5 @@
+# Define default links for this module.
+batch_example.form:
+ title: Batch API Examples
+ description: Batch examples using Drupal Batch API.
+ route_name: batch_example.form
diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.module b/sites/all/modules/examples/examples/batch_example/batch_example.module
new file mode 100644
index 000000000..c7873edc8
--- /dev/null
+++ b/sites/all/modules/examples/examples/batch_example/batch_example.module
@@ -0,0 +1,133 @@
+ $id, '@details' => $operation_details]
+ );
+}
+
+/**
+ * Batch operation for batch 2: five at a time.
+ *
+ * This is the function that is called on each operation in batch 2.
+ *
+ * After each group of 5 control is returned to the batch API for later
+ * continuation.
+ */
+function batch_example_op_2($operation_details, &$context) {
+ // Use the $context['sandbox'] at your convenience to store the
+ // information needed to track progression between successive calls.
+ if (empty($context['sandbox'])) {
+ $context['sandbox'] = [];
+ $context['sandbox']['progress'] = 0;
+ $context['sandbox']['current_node'] = 0;
+
+ // Save node count for the termination message.
+ $context['sandbox']['max'] = 30;
+ }
+
+ // Process in groups of 5 (arbitrary value).
+ // When a group of five is processed, the batch update engine determines
+ // whether it should continue processing in the same request or provide
+ // progress feedback to the user and wait for the next request.
+ // That way even though we're already processing at the operation level
+ // the operation itself is interruptible.
+ $limit = 5;
+
+ // Retrieve the next group.
+ $result = range($context['sandbox']['current_node'] + 1, $context['sandbox']['current_node'] + 1 + $limit);
+
+ foreach ($result as $row) {
+ // Here we actually perform our dummy 'processing' on the current node.
+ usleep(20000);
+
+ // Store some results for post-processing in the 'finished' callback.
+ // The contents of 'results' will be available as $results in the
+ // 'finished' function (in this example, batch_example_finished()).
+ $context['results'][] = $row . ' ' . $operation_details;
+
+ // Update our progress information.
+ $context['sandbox']['progress']++;
+ $context['sandbox']['current_node'] = $row;
+ $context['message'] = t('Running Batch "@id" @details',
+ ['@id' => $row, '@details' => $operation_details]
+ );
+ }
+
+ // Inform the batch engine that we are not finished,
+ // and provide an estimation of the completion level we reached.
+ if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+ $context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']);
+ }
+}
+
+/**
+ * Batch 'finished' callback used by both batch 1 and batch 2.
+ */
+function batch_example_finished($success, $results, $operations) {
+ if ($success) {
+ // Here we could do something meaningful with the results.
+ // We just display the number of nodes we processed...
+ drupal_set_message(t('@count results processed.', ['@count' => count($results)]));
+ drupal_set_message(t('The final result was "%final"', ['%final' => end($results)]));
+ }
+ else {
+ // An error occurred.
+ // $operations contains the operations that remained unprocessed.
+ $error_operation = reset($operations);
+ drupal_set_message(
+ t('An error occurred while processing @operation with arguments : @args',
+ [
+ '@operation' => $error_operation[0],
+ '@args' => print_r($error_operation[0], TRUE),
+ ]
+ )
+ );
+ }
+}
+
+/**
+ * @} End of "defgroup batch_example".
+ */
diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.routing.yml b/sites/all/modules/examples/examples/batch_example/batch_example.routing.yml
new file mode 100644
index 000000000..d0cb320eb
--- /dev/null
+++ b/sites/all/modules/examples/examples/batch_example/batch_example.routing.yml
@@ -0,0 +1,7 @@
+batch_example.form:
+ path: 'examples/batch_example'
+ defaults:
+ _form: '\Drupal\batch_example\Form\BatchExampleForm'
+ _title: 'Demo of batch processing'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/batch_example/src/Form/BatchExampleForm.php b/sites/all/modules/examples/examples/batch_example/src/Form/BatchExampleForm.php
new file mode 100644
index 000000000..476fdda63
--- /dev/null
+++ b/sites/all/modules/examples/examples/batch_example/src/Form/BatchExampleForm.php
@@ -0,0 +1,148 @@
+ 'markup',
+ '#markup' => t('This example offers two different batches. The first does 1000 identical operations, each completed in on run; the second does 20 operations, but each takes more than one run to operate if there are more than 5 nodes.'),
+ ];
+ $form['batch'] = [
+ '#type' => 'select',
+ '#title' => 'Choose batch',
+ '#options' => [
+ 'batch_1' => t('batch 1 - 1000 operations'),
+ 'batch_2' => t('batch 2 - 20 operations.'),
+ ],
+ ];
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => 'Go',
+ ];
+
+ return $form;
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Gather our form value.
+ $value = $form_state->getValues()['batch'];
+ // Set the batch, using convenience methods.
+ $batch = [];
+ switch ($value) {
+ case 'batch_1':
+ $batch = $this->generateBatch1();
+ break;
+
+ case 'batch_2':
+ $batch = $this->generateBatch2();
+ break;
+ }
+ batch_set($batch);
+ }
+
+ /**
+ * Generate Batch 1.
+ *
+ * Batch 1 will process one item at a time.
+ *
+ * This creates an operations array defining what batch 1 should do, including
+ * what it should do when it's finished. In this case, each operation is the
+ * same and by chance even has the same $nid to operate on, but we could have
+ * a mix of different types of operations in the operations array.
+ */
+ public function generateBatch1() {
+ $num_operations = 1000;
+ drupal_set_message(t('Creating an array of @num operations', ['@num' => $num_operations]));
+
+ $operations = [];
+ // Set up an operations array with 1000 elements, each doing function
+ // batch_example_op_1.
+ // Each operation in the operations array means at least one new HTTP
+ // request, running Drupal from scratch to accomplish the operation. If the
+ // operation returns with $context['finished'] != TRUE, then it will be
+ // called again.
+ // In this example, $context['finished'] is always TRUE.
+ for ($i = 0; $i < $num_operations; $i++) {
+ // Each operation is an array consisting of
+ // - The function to call.
+ // - An array of arguments to that function.
+ $operations[] = [
+ 'batch_example_op_1',
+ [
+ $i + 1,
+ t('(Operation @operation)', ['@operation' => $i]),
+ ],
+ ];
+ }
+ $batch = [
+ 'title' => t('Creating an array of @num operations', ['@num' => $num_operations]),
+ 'operations' => $operations,
+ 'finished' => 'batch_example_finished',
+ ];
+ return $batch;
+ }
+
+ /**
+ * Generate Batch 2.
+ *
+ * Batch 2 will process five items at a time.
+ *
+ * This creates an operations array defining what batch 2 should do, including
+ * what it should do when it's finished. In this case, each operation is the
+ * same and by chance even has the same $nid to operate on, but we could have
+ * a mix of different types of operations in the operations array.
+ */
+ public function generateBatch2() {
+ $num_operations = 20;
+
+ $operations = [];
+ // 20 operations, each one loads all nodes.
+ for ($i = 0; $i < $num_operations; $i++) {
+ $operations[] = [
+ 'batch_example_op_2',
+ [t('(Operation @operation)', ['@operation' => $i])],
+ ];
+ }
+ $batch = [
+ 'operations' => $operations,
+ 'finished' => 'batch_example_finished',
+ // @current, @remaining, @total, @percentage, @estimate and @elapsed.
+ // These placeholders are replaced with actual values in _batch_process(),
+ // using strtr() instead of t(). The values are determined based on the
+ // number of operations in the 'operations' array (above), NOT by the
+ // number of nodes that will be processed. In this example, there are 20
+ // operations, so @total will always be 20, even though there are multiple
+ // nodes per operation.
+ // Defaults to t('Completed @current of @total.').
+ 'title' => t('Processing batch 2'),
+ 'init_message' => t('Batch 2 is starting.'),
+ 'progress_message' => t('Processed @current out of @total.'),
+ 'error_message' => t('Batch 2 has encountered an error.'),
+ ];
+ return $batch;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/batch_example/tests/src/functional/BatchExampleWebTest.php b/sites/all/modules/examples/examples/batch_example/tests/src/functional/BatchExampleWebTest.php
new file mode 100644
index 000000000..687a330ae
--- /dev/null
+++ b/sites/all/modules/examples/examples/batch_example/tests/src/functional/BatchExampleWebTest.php
@@ -0,0 +1,40 @@
+drupalCreateUser(['access content']);
+ $this->drupalLogin($web_user);
+
+ // Launch Batch 1.
+ $this->drupalPostForm('examples/batch_example', ['batch' => 'batch_1'], 'Go');
+ // Check that 1000 operations were performed.
+ $this->assertText('1000 results processed');
+
+ // Launch Batch 2.
+ $this->drupalPostForm('examples/batch_example', ['batch' => 'batch_2'], 'Go');
+ // Check that 600 operations were performed.
+ $this->assertText('600 results processed');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/block_example/block_example.info.yml b/sites/all/modules/examples/examples/block_example/block_example.info.yml
new file mode 100644
index 000000000..8c03440da
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/block_example.info.yml
@@ -0,0 +1,15 @@
+name: Block Example
+type: module
+description: Demonstrates how a module can define blocks.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:block
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/block_example/block_example.links.menu.yml b/sites/all/modules/examples/examples/block_example/block_example.links.menu.yml
new file mode 100644
index 000000000..4ec1c1ebc
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/block_example.links.menu.yml
@@ -0,0 +1,3 @@
+block_example.description:
+ title: Block Example
+ route_name: block_example.description
diff --git a/sites/all/modules/examples/examples/block_example/block_example.module b/sites/all/modules/examples/examples/block_example/block_example.module
new file mode 100644
index 000000000..a5ba84b5b
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/block_example.module
@@ -0,0 +1,53 @@
+getPluginDefinition();
+ if ((!empty($build['#configuration']['label']) && stristr($build['#configuration']['label'], 'uppercase')) || (!empty($definition['subject']) && stristr($definition['subject'], 'uppercase'))) {
+ // This will uppercase the block title.
+ $build['#configuration']['label'] = Unicode::strtoupper($build['#configuration']['label']);
+ }
+}
+
+/**
+ * @} End of "defgroup block_example".
+ */
diff --git a/sites/all/modules/examples/examples/block_example/block_example.routing.yml b/sites/all/modules/examples/examples/block_example/block_example.routing.yml
new file mode 100644
index 000000000..975fe5b1d
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/block_example.routing.yml
@@ -0,0 +1,7 @@
+block_example.description:
+ path: '/examples/block-example'
+ defaults:
+ _controller: '\Drupal\block_example\Controller\BlockExampleController::description'
+ _title: 'Block Example'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/block_example/config/schema/block_example.schema.yml b/sites/all/modules/examples/examples/block_example/config/schema/block_example.schema.yml
new file mode 100644
index 000000000..0de8cc82e
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/config/schema/block_example.schema.yml
@@ -0,0 +1,7 @@
+block.settings.example_configurable_text:
+ type: block_settings
+ label: 'Example configurable text block configuration'
+ mapping:
+ block_example_string:
+ type: text
+ label: 'Block contents'
diff --git a/sites/all/modules/examples/examples/block_example/src/Controller/BlockExampleController.php b/sites/all/modules/examples/examples/block_example/src/Controller/BlockExampleController.php
new file mode 100644
index 000000000..78333251c
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/src/Controller/BlockExampleController.php
@@ -0,0 +1,19 @@
+ $this->t('A default value. This block was created at %time', ['%time' => date('c')]),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * This method defines form elements for custom block configuration. Standard
+ * block configuration fields are added by BlockBase::buildConfigurationForm()
+ * (block title and title visibility) and BlockFormController::form() (block
+ * visibility settings).
+ *
+ * @see \Drupal\block\BlockBase::buildConfigurationForm()
+ * @see \Drupal\block\BlockFormController::form()
+ */
+ public function blockForm($form, FormStateInterface $form_state) {
+ $form['block_example_string_text'] = [
+ '#type' => 'textarea',
+ '#title' => $this->t('Block contents'),
+ '#description' => $this->t('This text will appear in the example block.'),
+ '#default_value' => $this->configuration['block_example_string'],
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * This method processes the blockForm() form fields when the block
+ * configuration form is submitted.
+ *
+ * The blockValidate() method can be used to validate the form submission.
+ */
+ public function blockSubmit($form, FormStateInterface $form_state) {
+ $this->configuration['block_example_string']
+ = $form_state->getValue('block_example_string_text');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ return [
+ '#markup' => $this->configuration['block_example_string'],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleEmptyBlock.php b/sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleEmptyBlock.php
new file mode 100644
index 000000000..b14facd0d
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleEmptyBlock.php
@@ -0,0 +1,30 @@
+ t("This block's title is changed to uppercase. Any block title which contains 'uppercase' will also be changed to uppercase."),
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/block_example/templates/description.html.twig b/sites/all/modules/examples/examples/block_example/templates/description.html.twig
new file mode 100644
index 000000000..e3e188522
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/templates/description.html.twig
@@ -0,0 +1,15 @@
+{#
+
+Description text for the Block Example.
+
+#}
+
+{% set block_admin_page = url('block.admin_display')['#markup'] %}
+
+{% trans %}
+
+
The Block Example provides three sample blocks which demonstrate the various
+ block APIs. To experiment with the blocks, enable and configure them on
+ the block admin page.
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleMenuTest.php b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleMenuTest.php
new file mode 100644
index 000000000..e7854403b
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleMenuTest.php
@@ -0,0 +1,50 @@
+drupalGet('');
+ $this->assertLinkByHref('examples/block-example');
+
+ $this->drupalGet('examples/block-example');
+ $this->assertResponse(200, 'Description page exists.');
+
+ // Verify that the block admin page link works.
+ $this->clickLink('the block admin page');
+ // Since it links to the admin page, we should get a permissions error and
+ // not 404.
+ $this->assertResponse(403);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleTest.php b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleTest.php
new file mode 100644
index 000000000..fb2c5e897
--- /dev/null
+++ b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleTest.php
@@ -0,0 +1,87 @@
+assertSession();
+
+ // Create user.
+ $web_user = $this->drupalCreateUser(['administer blocks']);
+ // Login the admin user.
+ $this->drupalLogin($web_user);
+
+ $theme_name = \Drupal::config('system.theme')->get('default');
+
+ // Verify the blocks are listed to be added.
+ $this->drupalGet('/admin/structure/block/library/' . $theme_name, ['query' => ['region' => 'content']]);
+ $assert->pageTextContains('Example: configurable text');
+ $assert->pageTextContains('Example: empty block');
+ $assert->pageTextContains('Example: uppercase this please');
+
+ // Define and place blocks.
+ $settings_configurable = [
+ 'label' => 'Configurable text',
+ 'id' => 'block_example_example_configurable_text',
+ 'theme' => $theme_name,
+ ];
+ $this->drupalPlaceBlock('example_configurable_text', $settings_configurable);
+
+ $settings_uppercase = [
+ 'label' => 'Configurable block to be uppercased',
+ 'id' => 'block_example_example_uppercased',
+ 'theme' => $theme_name,
+ ];
+ $this->drupalPlaceBlock('example_uppercase', $settings_uppercase);
+
+ $settings_empty = [
+ 'label' => 'Empty block',
+ 'id' => 'block_example_example_empty',
+ 'theme' => $theme_name,
+ ];
+ $this->drupalPlaceBlock('example_empty', $settings_empty);
+
+ // Verify that blocks are there. Empty block will not be shown, because it
+ // holds an empty array.
+ $this->drupalGet('');
+ $assert->pageTextContains($settings_configurable['label']);
+ $assert->pageTextContains($settings_uppercase['label']);
+ $assert->pageTextContains(Unicode::strtoupper($settings_uppercase['label']));
+ $assert->pageTextNotContains($settings_empty['label']);
+
+ // Change content of configurable text block.
+ $edit = [
+ 'settings[block_example_string_text]' => $this->randomMachineName(),
+ ];
+ $this->drupalPostForm('/admin/structure/block/manage/' . $settings_configurable['id'], $edit, 'Save block');
+ $assert->statusCodeEquals(200);
+
+ // Verify that new content is shown.
+ $this->drupalGet('');
+ $assert->statusCodeEquals(200);
+ $assert->pageTextContains($edit['settings[block_example_string_text]']);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/cache_example/cache_example.info.yml b/sites/all/modules/examples/examples/cache_example/cache_example.info.yml
new file mode 100644
index 000000000..9bf94e41d
--- /dev/null
+++ b/sites/all/modules/examples/examples/cache_example/cache_example.info.yml
@@ -0,0 +1,14 @@
+name: Cache Example
+description: Demonstrates how to use Cache API.
+package: Example modules
+# core: 8.x
+type: module
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/cache_example/cache_example.links.menu.yml b/sites/all/modules/examples/examples/cache_example/cache_example.links.menu.yml
new file mode 100644
index 000000000..2e148950d
--- /dev/null
+++ b/sites/all/modules/examples/examples/cache_example/cache_example.links.menu.yml
@@ -0,0 +1,4 @@
+cache_example.description:
+ title: 'Cache Example'
+ description: 'Example of Drupal Cache API'
+ route_name: cache_example.description
diff --git a/sites/all/modules/examples/examples/cache_example/cache_example.module b/sites/all/modules/examples/examples/cache_example/cache_example.module
new file mode 100644
index 000000000..626606ceb
--- /dev/null
+++ b/sites/all/modules/examples/examples/cache_example/cache_example.module
@@ -0,0 +1,36 @@
+setRequestStack($request_stack);
+ $this->setStringTranslation($translation);
+ $this->currentUser = $current_user;
+ $this->cacheBackend = $cache_backend;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ // Forms that require a Drupal service or a custom service should access
+ // the service using dependency injection.
+ // @link https://www.drupal.org/node/2203931.
+ // Those services are passed in the $container through the static create
+ // method.
+ return new static(
+ $container->get('request_stack'),
+ $container->get('string_translation'),
+ $container->get('current_user'),
+ $container->get('cache.default')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'cron_cache';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ // Log execution time.
+ $start_time = microtime(TRUE);
+
+ // Try to load the files count from cache. This function will accept two
+ // arguments:
+ // - cache object name (cid)
+ // - cache bin, the (optional) cache bin (most often a database table) where
+ // the object is to be saved.
+ //
+ // cache_get() returns the cached object or FALSE if object does not exist.
+ if ($cache = $this->cacheBackend->get('cache_example_files_count')) {
+ /*
+ * Get cached data. Complex data types will be unserialized automatically.
+ */
+ $files_count = $cache->data;
+ }
+ else {
+ // If there was no cached data available we have to search filesystem.
+ // Recursively get all .PHP files from Drupal's core folder.
+ $files_count = count(file_scan_directory('core', '/.php/'));
+
+ // Since we have recalculated, we now need to store the new data into
+ // cache. Complex data types will be automatically serialized before
+ // being saved into cache.
+ // Here we use the default setting and create an unexpiring cache item.
+ // See below for an example that creates an expiring cache item.
+ $this->cacheBackend->set('cache_example_files_count', $files_count, CacheBackendInterface::CACHE_PERMANENT);
+ }
+
+ $end_time = microtime(TRUE);
+ $duration = $end_time - $start_time;
+
+ // Format intro message.
+ $intro_message = '
' . $this->t("This example will search Drupal's core folder and display a count of the PHP files in it.") . ' ';
+ $intro_message .= $this->t('This can take a while, since there are a lot of files to be searched.') . ' ';
+ $intro_message .= $this->t('We will search filesystem just once and save output to the cache. We will use cached data for later requests.') . '
';
+ $intro_message .= '
'
+ . $this->t(
+ 'Reload this page to see cache in action.',
+ ['@url' => $this->getRequest()->getRequestUri()]
+ )
+ . ' ';
+ $intro_message .= $this->t('You can use the button below to remove cached data.') . '
';
+
+ $form['file_search'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('File search caching'),
+ ];
+ $form['file_search']['introduction'] = [
+ '#markup' => $intro_message,
+ ];
+
+ $color = empty($cache) ? 'red' : 'green';
+ $retrieval = empty($cache) ? $this->t('calculated by traversing the filesystem') : $this->t('retrieved from cache');
+
+ $form['file_search']['statistics'] = [
+ '#type' => 'item',
+ '#markup' => $this->t('%count files exist in this Drupal installation; @retrieval in @time ms. (Source: @source)', [
+ '%count' => $files_count,
+ '@retrieval' => $retrieval,
+ '@time' => number_format($duration * 1000, 2),
+ '@color' => $color,
+ '@source' => empty($cache) ? $this->t('actual file search') : $this->t('cached'),
+ ]
+ ),
+ ];
+ $form['file_search']['remove_file_count'] = [
+ '#type' => 'submit',
+ '#submit' => [[$this, 'expireFiles']],
+ '#value' => $this->t('Explicitly remove cached file count'),
+ ];
+
+ $form['expiration_demo'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Cache expiration settings'),
+ ];
+ $form['expiration_demo']['explanation'] = [
+ '#markup' => $this->t('A cache item can be set as CACHE_PERMANENT, meaning that it will only be removed when explicitly cleared, or it can have an expiration time (a Unix timestamp).'),
+ ];
+
+ $item = $this->cacheBackend->get('cache_example_expiring_item', TRUE);
+ if ($item == FALSE) {
+ $item_status = $this->t('Cache item does not exist');
+ }
+ else {
+ $item_status = $item->valid ? $this->t('Cache item exists and is set to expire at %time', ['%time' => $item->data]) :
+ $this->t('Cache_item is invalid');
+ }
+
+ $form['expiration_demo']['current_status'] = [
+ '#type' => 'item',
+ '#title' => $this->t('Current status of cache item "cache_example_expiring_item"'),
+ '#markup' => $item_status,
+ ];
+ $form['expiration_demo']['expiration'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Time before cache expiration'),
+ '#options' => [
+ 'never_remove' => $this->t('CACHE_PERMANENT'),
+ -10 => $this->t('Immediate expiration'),
+ 10 => $this->t('10 seconds from form submission'),
+ 60 => $this->t('1 minute from form submission'),
+ 300 => $this->t('5 minutes from form submission'),
+ ],
+ '#default_value' => -10,
+ '#description' => $this->t('Any cache item can be set to only expire when explicitly cleared, or to expire at a given time.'),
+ ];
+ $form['expiration_demo']['create_cache_item'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Create a cache item with this expiration'),
+ '#submit' => [[$this, 'createExpiringItem']],
+ ];
+
+ $form['cache_clearing'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Expire and remove options'),
+ '#description' => $this->t("We have APIs to expire cached items and also to just remove them. Unfortunately, they're all the same API, cache_clear_all"),
+ ];
+ $form['cache_clearing']['cache_clear_type'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Type of cache clearing to do'),
+ '#options' => [
+ 'expire' => $this->t('Remove items from the "cache" bin that have expired'),
+ 'remove_all' => $this->t('Remove all items from the "cache" bin regardless of expiration'),
+ 'remove_tag' => $this->t('Remove all items in the "cache" bin with the tag "cache_example" set to 1'),
+ ],
+ '#default_value' => 'expire',
+ ];
+ // Submit button to clear cached data.
+ $form['cache_clearing']['clear_expired'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Clear or expire cache'),
+ '#submit' => [[$this, 'cacheClearing']],
+ '#access' => $this->currentUser->hasPermission('administer site configuration'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Submit handler that explicitly clears cache_example_files_count from cache.
+ */
+ public function expireFiles($form, &$form_state) {
+ // Clear cached data. This function will delete cached object from cache
+ // bin.
+ //
+ // The first argument is cache id to be deleted. Since we've provided it
+ // explicitly, it will be removed whether or not it has an associated
+ // expiration time. The second argument (required here) is the cache bin.
+ // Using cache_clear_all() explicitly in this way
+ // forces removal of the cached item.
+ $this->cacheBackend->delete('cache_example_files_count');
+
+ // Display message to the user.
+ drupal_set_message($this->t('Cached data key "cache_example_files_count" was cleared.'), 'status');
+ }
+
+ /**
+ * Submit handler to create a new cache item with specified expiration.
+ */
+ public function createExpiringItem($form, &$form_state) {
+
+ $tags = [
+ 'cache_example:1',
+ ];
+
+ $interval = $form_state->getValue('expiration');
+ if ($interval == 'never_remove') {
+ $expiration = CacheBackendInterface::CACHE_PERMANENT;
+ $expiration_friendly = $this->t('Never expires');
+ }
+ else {
+ $expiration = time() + $interval;
+ $expiration_friendly = format_date($expiration);
+ }
+ // Set the expiration to the actual Unix timestamp of the end of the
+ // required interval. Also add a tag to it to be able to clear caches more
+ // precise.
+ $this->cacheBackend->set('cache_example_expiring_item', $expiration_friendly, $expiration, $tags);
+ drupal_set_message($this->t('cache_example_expiring_item was set to expire at %time', ['%time' => $expiration_friendly]));
+ }
+
+ /**
+ * Submit handler to demonstrate the various uses of cache_clear_all().
+ */
+ public function cacheClearing($form, &$form_state) {
+ switch ($form_state->getValue('cache_clear_type')) {
+ case 'expire':
+ // Here we'll remove all cache keys in the 'cache' bin that have
+ // expired.
+ $this->cacheBackend->garbageCollection();
+ drupal_set_message($this->t('\Drupal::cache()->garbageCollection() was called, removing any expired cache items.'));
+ break;
+
+ case 'remove_all':
+ // This removes all keys in a bin using a super-wildcard. This
+ // has nothing to do with expiration. It's just brute-force removal.
+ $this->cacheBackend->deleteAll();
+ drupal_set_message($this->t('ALL entries in the "cache" bin were removed with \Drupal::cache()->deleteAll().'));
+ break;
+
+ case 'remove_tag':
+ // This removes cache entries with the tag "cache_example" set to 1 in
+ // the "cache".
+ $tags = [
+ 'cache_example:1',
+ ];
+ Cache::invalidateTags($tags);
+ drupal_set_message($this->t('Cache entries with the tag "cache_example" set to 1 in the "cache" bin were invalidated with \Drupal\Core\Cache\Cache::invalidateTags($tags).'));
+ break;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/cache_example/tests/src/Functional/CacheExampleTest.php b/sites/all/modules/examples/examples/cache_example/tests/src/Functional/CacheExampleTest.php
new file mode 100644
index 000000000..86b9bc2c4
--- /dev/null
+++ b/sites/all/modules/examples/examples/cache_example/tests/src/Functional/CacheExampleTest.php
@@ -0,0 +1,110 @@
+assertSession();
+
+ // Test for a link to the cache_example in the Tools menu.
+ $this->drupalGet('');
+ $assert->statusCodeEquals(200);
+
+ $assert->linkByHrefExists('examples/cache-example');
+
+ // Verify if the can successfully access the cache_example form.
+ $this->drupalGet('examples/cache-example');
+ $assert->statusCodeEquals(200);
+ }
+
+ /**
+ * Test that our caches function.
+ *
+ * Does the following:
+ * - Load cache example page and test if displaying uncached version.
+ * - Reload once again and test if displaying cached version.
+ * - Find reload link and click on it.
+ * - Clear cache at the end and test if displaying uncached version again.
+ */
+ public function testCacheExampleBasic() {
+ $assert = $this->assertSession();
+
+ // We need administrative privileges to clear the cache.
+ $admin_user = $this->drupalCreateUser(['administer site configuration']);
+ $this->drupalLogin($admin_user);
+
+ // Get initial page cache example page, first time accessed,
+ // and assert uncached output.
+ $this->drupalGet('examples/cache-example');
+ $assert->pageTextContains('Source: actual file search');
+
+ // Reload the page; the number should be cached.
+ $this->drupalGet('examples/cache-example');
+ $assert->pageTextContains('Source: cached');
+
+ // Now push the button to remove the count.
+ $this->drupalPostForm('examples/cache-example', [], 'Explicitly remove cached file count');
+ $assert->pageTextContains('Source: actual file search');
+
+ // Create a cached item. First make sure it doesn't already exist.
+ $assert->pageTextContains('Cache item does not exist');
+ $this->drupalPostForm('examples/cache-example', ['expiration' => -10], 'Create a cache item with this expiration');
+ // We should now have an already-expired item. Automatically invalid.
+ $assert->pageTextContains('Cache_item is invalid');
+ // Now do the expiration operation.
+ $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'expire'], 'Clear or expire cache');
+ // And verify that it was removed.
+ $assert->pageTextContains('Cache item does not exist');
+
+ // Create a cached item. This time we'll make it not expire.
+ $this->drupalPostForm('examples/cache-example', ['expiration' => 'never_remove'], 'Create a cache item with this expiration');
+ // We should now have an never-remove item.
+ $assert->pageTextContains('Cache item exists and is set to expire at Never expires');
+ // Now do the expiration operation.
+ $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'expire'], 'Clear or expire cache');
+ // And verify that it was not removed.
+ $assert->pageTextContains('Cache item exists and is set to expire at Never expires');
+ // Now do tag invalidation.
+ $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'remove_tag'], 'Clear or expire cache');
+ // And verify that it was invalidated.
+ $assert->pageTextContains('Cache_item is invalid');
+ // Do the hard delete.
+ $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'remove_all'], 'Clear or expire cache');
+ // And verify that it was removed.
+ $assert->pageTextContains('Cache item does not exist');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/composer.json b/sites/all/modules/examples/examples/composer.json
new file mode 100644
index 000000000..26d4063ab
--- /dev/null
+++ b/sites/all/modules/examples/examples/composer.json
@@ -0,0 +1,12 @@
+{
+ "name": "drupal/examples",
+ "description": "The Examples for Developers project aims to provide high-quality, well-documented API examples for a broad range of Drupal core functionality.",
+ "type": "drupal-module",
+ "homepage": "https://www.drupal.org/project/examples",
+ "support": {
+ "issues": "https://www.drupal.org/project/issues/examples",
+ "documentation": "https://api.drupal.org/api/examples",
+ "source": "http://cgit.drupalcode.org/examples"
+ },
+ "license": "GPL-2.0+"
+}
diff --git a/sites/all/modules/examples/examples/config_entity_example/config/install/config_entity_example.robot.marvin.yml b/sites/all/modules/examples/examples/config_entity_example/config/install/config_entity_example.robot.marvin.yml
new file mode 100644
index 000000000..1a7b1522e
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/config/install/config_entity_example.robot.marvin.yml
@@ -0,0 +1,20 @@
+# This file defines a default config entity. This allows the module to include
+# config entities that are present 'out of the box'. Default config entities
+# are created in Drupal when the module is enabled. They are removed when the
+# module is uninstalled.
+
+# Default config entities can be edited by the user within Drupal. This edited
+# entity wil become exportable through the configuration system. This file,
+# however, will remain untouched.
+
+# For our config entity to be added at installation time, we have to place it in
+# the config/install directory of our module. Thus our file is located at:
+# config/install/config_entity_example.robot.marvin.yml.
+
+# You can see where these properties are defined in the annotation of
+# Drupal\config_entity_example\Entity\Robot.
+
+# The id of the config entity.
+id: marvin
+# Our properties follow.
+label: 'Marvin, the paranoid android'
diff --git a/sites/all/modules/examples/examples/config_entity_example/config/schema/config_entity_example.schema.yml b/sites/all/modules/examples/examples/config_entity_example/config/schema/config_entity_example.schema.yml
new file mode 100644
index 000000000..08121765d
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/config/schema/config_entity_example.schema.yml
@@ -0,0 +1,28 @@
+# Schema for the configuration files of the Config Entity Example module.
+
+# This schema tells the config system how to read our config YML files.
+# See for example the file config/config_entity_example.robot.marvin.yml, which
+# contains our default config entity.
+
+# Documentation for schema files like this one is located here:
+# https://drupal.org/node/1905070
+
+config_entity_example.robot.*:
+ type: mapping
+ label: 'Robot'
+ mapping:
+ id:
+ type: string
+ label: 'Robot id'
+ uuid:
+ type: string
+ label: 'UUID'
+ label:
+ type: label
+ label: 'Label'
+ floopy:
+ type: boolean
+ label: 'Floopy'
+ langcode:
+ type: string
+ label: 'Default language'
diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.info.yml b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.info.yml
new file mode 100644
index 000000000..e517641e5
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.info.yml
@@ -0,0 +1,13 @@
+name: 'Config Entity Example'
+type: module
+description: 'Demonstrates how to create a config entity type.'
+package: Example modules
+# core: 8.x
+dependencies:
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.action.yml b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.action.yml
new file mode 100644
index 000000000..e55bd4af6
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.action.yml
@@ -0,0 +1,14 @@
+# Add some local task links to facilitate navigation.
+
+config_entity_example.add_action:
+ route_name: entity.robot.add_form
+ title: 'Add robot'
+ appears_on:
+ - entity.robot.list
+
+config_entity_example.list_action:
+ route_name: entity.robot.list
+ title: 'List robots'
+ appears_on:
+ - entity.robot.add_form
+ - entity.robot.edit_form
diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.menu.yml b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.menu.yml
new file mode 100644
index 000000000..328da767d
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.menu.yml
@@ -0,0 +1,3 @@
+config_entity_example.menu:
+ title: Config Entity Example
+ route_name: entity.robot.list
diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.module b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.module
new file mode 100644
index 000000000..251c65922
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.module
@@ -0,0 +1,42 @@
+t('Robot');
+ $header['machine_name'] = $this->t('Machine Name');
+ $header['floopy'] = $this->t('Floopy');
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * Builds a row for an entity in the entity listing.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity for which to build the row.
+ *
+ * @return array
+ * A render array of the table row for displaying the entity.
+ *
+ * @see \Drupal\Core\Entity\EntityListController::render()
+ */
+ public function buildRow(EntityInterface $entity) {
+ $row['label'] = $entity->label();
+ $row['machine_name'] = $entity->id();
+ $row['floopy'] = $entity->floopy;
+
+ return $row + parent::buildRow($entity);
+ }
+
+ /**
+ * Adds some descriptive text to our entity list.
+ *
+ * Typically, there's no need to override render(). You may wish to do so,
+ * however, if you want to add markup before or after the table.
+ *
+ * @return array
+ * Renderable array.
+ */
+ public function render() {
+ $build = $this->description();
+ $build[] = parent::render();
+ return $build;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Entity/Robot.php b/sites/all/modules/examples/examples/config_entity_example/src/Entity/Robot.php
new file mode 100644
index 000000000..f9286b030
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/src/Entity/Robot.php
@@ -0,0 +1,89 @@
+t('Create Robot');
+ return $actions;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotDeleteForm.php b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotDeleteForm.php
new file mode 100644
index 000000000..99bdae753
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotDeleteForm.php
@@ -0,0 +1,89 @@
+t('Are you sure you want to delete robot %label?', [
+ '%label' => $this->entity->label(),
+ ]);
+ }
+
+ /**
+ * Gather the confirmation text.
+ *
+ * The confirm text is used as the text in the button that confirms the
+ * question posed by getQuestion().
+ *
+ * @return string
+ * Translated string.
+ */
+ public function getConfirmText() {
+ return $this->t('Delete Robot');
+ }
+
+ /**
+ * Gets the cancel URL.
+ *
+ * Provides the URL to go to if the user cancels the action. For entity
+ * delete forms, this is typically the route that points at the list
+ * controller.
+ *
+ * @return \Drupal\Core\Url
+ * The URL to go to if the user cancels the deletion.
+ */
+ public function getCancelUrl() {
+ return new Url('entity.robot.list');
+ }
+
+ /**
+ * The submit handler for the confirm form.
+ *
+ * For entity delete forms, you use this to delete the entity in
+ * $this->entity.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * An associative array containing the current state of the form.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Delete the entity.
+ $this->entity->delete();
+
+ // Set a message that the entity was deleted.
+ drupal_set_message($this->t('Robot %label was deleted.', [
+ '%label' => $this->entity->label(),
+ ]));
+
+ // Redirect the user to the list controller when complete.
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotEditForm.php b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotEditForm.php
new file mode 100644
index 000000000..eda096f63
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotEditForm.php
@@ -0,0 +1,35 @@
+t('Update Robot');
+ return $actions;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotFormBase.php b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotFormBase.php
new file mode 100644
index 000000000..f6afad05f
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotFormBase.php
@@ -0,0 +1,221 @@
+entityQueryFactory = $query_factory;
+ }
+
+ /**
+ * Factory method for RobotFormBase.
+ *
+ * When Drupal builds this class it does not call the constructor directly.
+ * Instead, it relies on this method to build the new object. Why? The class
+ * constructor may take multiple arguments that are unknown to Drupal. The
+ * create() method always takes one parameter -- the container. The purpose
+ * of the create() method is twofold: It provides a standard way for Drupal
+ * to construct the object, meanwhile it provides you a place to get needed
+ * constructor parameters from the container.
+ *
+ * In this case, we ask the container for an entity query factory. We then
+ * pass the factory to our class as a constructor parameter.
+ */
+ public static function create(ContainerInterface $container) {
+ return new static($container->get('entity.query'));
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\EntityFormController::form().
+ *
+ * Builds the entity add/edit form.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * An associative array containing the current state of the form.
+ *
+ * @return array
+ * An associative array containing the robot add/edit form.
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ // Get anything we need from the base class.
+ $form = parent::buildForm($form, $form_state);
+
+ // Drupal provides the entity to us as a class variable. If this is an
+ // existing entity, it will be populated with existing values as class
+ // variables. If this is a new entity, it will be a new object with the
+ // class of our entity. Drupal knows which class to call from the
+ // annotation on our Robot class.
+ $robot = $this->entity;
+
+ // Build the form.
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => $robot->label(),
+ '#required' => TRUE,
+ ];
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#title' => $this->t('Machine name'),
+ '#default_value' => $robot->id(),
+ '#machine_name' => [
+ 'exists' => [$this, 'exists'],
+ 'replace_pattern' => '([^a-z0-9_]+)|(^custom$)',
+ 'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".',
+ ],
+ '#disabled' => !$robot->isNew(),
+ ];
+ $form['floopy'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Floopy'),
+ '#default_value' => $robot->floopy,
+ ];
+
+ // Return the form.
+ return $form;
+ }
+
+ /**
+ * Checks for an existing robot.
+ *
+ * @param string|int $entity_id
+ * The entity ID.
+ * @param array $element
+ * The form element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return bool
+ * TRUE if this format already exists, FALSE otherwise.
+ */
+ public function exists($entity_id, array $element, FormStateInterface $form_state) {
+ // Use the query factory to build a new robot entity query.
+ $query = $this->entityQueryFactory->get('robot');
+
+ // Query the entity ID to see if its in use.
+ $result = $query->condition('id', $element['#field_prefix'] . $entity_id)
+ ->execute();
+
+ // We don't need to return the ID, only if it exists or not.
+ return (bool) $result;
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\EntityFormController::actions().
+ *
+ * To set the submit button text, we need to override actions().
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * An associative array containing the current state of the form.
+ *
+ * @return array
+ * An array of supported actions for the current entity form.
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ // Get the basic actins from the base class.
+ $actions = parent::actions($form, $form_state);
+
+ // Change the submit button text.
+ $actions['submit']['#value'] = $this->t('Save');
+
+ // Return the result.
+ return $actions;
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\EntityFormController::validate().
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * An associative array containing the current state of the form.
+ */
+ public function validate(array $form, FormStateInterface $form_state) {
+ parent::validate($form, $form_state);
+
+ // Add code here to validate your config entity's form elements.
+ // Nothing to do here.
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\EntityFormController::save().
+ *
+ * Saves the entity. This is called after submit() has built the entity from
+ * the form values. Do not override submit() as save() is the preferred
+ * method for entity form controllers.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * An associative array containing the current state of the form.
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ // EntityForm provides us with the entity we're working on.
+ $robot = $this->getEntity();
+
+ // Drupal already populated the form values in the entity object. Each
+ // form field was saved as a public variable in the entity class. PHP
+ // allows Drupal to do this even if the method is not defined ahead of
+ // time.
+ $status = $robot->save();
+
+ // Grab the URL of the new entity. We'll use it in the message.
+ $url = $robot->urlInfo();
+
+ // Create an edit link.
+ $edit_link = Link::fromTextAndUrl($this->t('Edit'), $url)->toString();
+
+ if ($status == SAVED_UPDATED) {
+ // If we edited an existing entity...
+ drupal_set_message($this->t('Robot %label has been updated.', ['%label' => $robot->label()]));
+ $this->logger('contact')->notice('Robot %label has been updated.', ['%label' => $robot->label(), 'link' => $edit_link]);
+ }
+ else {
+ // If we created a new entity...
+ drupal_set_message($this->t('Robot %label has been added.', ['%label' => $robot->label()]));
+ $this->logger('contact')->notice('Robot %label has been added.', ['%label' => $robot->label(), 'link' => $edit_link]);
+ }
+
+ // Redirect the user back to the listing route after the save operation.
+ $form_state->setRedirect('entity.robot.list');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/config_entity_example/src/RobotAccessController.php b/sites/all/modules/examples/examples/config_entity_example/src/RobotAccessController.php
new file mode 100644
index 000000000..e28b13e8f
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/src/RobotAccessController.php
@@ -0,0 +1,34 @@
+The Config Entity Example module defines a Robot entity type. This is a list
+ of the Robot entities currently in your Drupal site.
By default, when
+ you enable this module, one entity is created from configuration. This is
+ why we call them Config Entities. Marvin, the paranoid android, is created
+ in the database when the module is enabled.
You can view a list of
+ Robots here. You can also use the 'Operations' column to edit and delete
+ Robots.
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/config_entity_example/tests/src/Functional/ConfigEntityExampleTest.php b/sites/all/modules/examples/examples/config_entity_example/tests/src/Functional/ConfigEntityExampleTest.php
new file mode 100644
index 000000000..05e97c8d4
--- /dev/null
+++ b/sites/all/modules/examples/examples/config_entity_example/tests/src/Functional/ConfigEntityExampleTest.php
@@ -0,0 +1,172 @@
+assertSession();
+
+ // 1) Verify that the Marvin entity was created when the module was
+ // installed.
+ $entity = Robot::load('marvin');
+ $this->assertNotNull($entity, 'Marvin was created during installation.');
+
+ // 2) Verify that permissions are applied to the various defined paths.
+ // Define some paths. Since the Marvin entity is defined, we can use it
+ // in our management paths.
+ $forbidden_paths = [
+ '/examples/config-entity-example',
+ '/examples/config-entity-example/add',
+ '/examples/config-entity-example/manage/marvin',
+ '/examples/config-entity-example/manage/marvin/delete',
+ ];
+ // Check each of the paths to make sure we don't have access. At this point
+ // we haven't logged in any users, so the client is anonymous.
+ foreach ($forbidden_paths as $path) {
+ $this->drupalGet($path);
+ $assert->statusCodeEquals(403);
+ }
+
+ // Create a user with no permissions.
+ $noperms_user = $this->drupalCreateUser();
+ $this->drupalLogin($noperms_user);
+ // Should be the same result for forbidden paths, since the user needs
+ // special permissions for these paths.
+ foreach ($forbidden_paths as $path) {
+ $this->drupalGet($path);
+ $assert->statusCodeEquals(403);
+ }
+
+ // Create a user who can administer robots.
+ $admin_user = $this->drupalCreateUser(['administer robots']);
+ $this->drupalLogin($admin_user);
+ // Forbidden paths aren't forbidden any more.
+ foreach ($forbidden_paths as $unforbidden) {
+ $this->drupalGet($unforbidden);
+ $assert->statusCodeEquals(200);
+ }
+
+ // Now that we have the admin user logged in, check the menu links.
+ $this->drupalGet('');
+ $assert->linkByHrefExists('examples/config-entity-example');
+
+ // 3) Verify that we can manage entities through the user interface.
+ // We still have the admin user logged in, so we'll create, update, and
+ // delete an entity.
+ // Go to the list page.
+ $this->drupalGet('/examples/config-entity-example');
+ $this->clickLink('Add robot');
+ $robot_machine_name = 'roboname';
+ $this->drupalPostForm(
+ NULL,
+ [
+ 'label' => $robot_machine_name,
+ 'id' => $robot_machine_name,
+ 'floopy' => TRUE,
+ ],
+ 'Create Robot'
+ );
+
+ // 4) Verify that our robot appears when we edit it.
+ $this->drupalGet('/examples/config-entity-example/manage/' . $robot_machine_name);
+ $assert->fieldExists('label');
+ $assert->checkboxChecked('edit-floopy');
+
+ // 5) Verify that the label and machine name are shown in the list.
+ $this->drupalGet('/examples/config-entity-example');
+ $this->clickLink('Add robot');
+ $robby_machine_name = 'robby_machine_name';
+ $robby_label = 'Robby label';
+ $this->drupalPostForm(
+ NULL,
+ [
+ 'label' => $robby_label,
+ 'id' => $robby_machine_name,
+ 'floopy' => TRUE,
+ ],
+ 'Create Robot'
+ );
+ $this->drupalGet('/examples/config-entity-example');
+ $assert->pageTextContains($robby_label);
+ $assert->pageTextContains($robby_machine_name);
+
+ // 6) Verify that required links are present on respective paths.
+ $this->assertLinkByHref('/examples/config-entity-example/add');
+ $this->assertLinkByHref('/examples/config-entity-example/manage/robby_machine_name');
+ $this->assertLinkByHref('/examples/config-entity-example/manage/robby_machine_name/delete');
+
+ // Verify links on Add Robot.
+ $this->drupalGet('/examples/config-entity-example/add');
+ $this->assertActionButton('examples/config-entity-example');
+
+ // Verify links on Edit Robot.
+ $this->drupalGet('/examples/config-entity-example/manage/robby_machine_name');
+ $this->assertLinkByHref('/examples/config-entity-example/manage/robby_machine_name/delete');
+ $this->assertActionButton('examples/config-entity-example');
+
+ // Verify links on Delete Robot.
+ $this->drupalGet('/examples/config-entity-example/manage/robby_machine_name/delete');
+ // List page will be the destination of the cancel link.
+ $cancel_button = $this->xpath(
+ '//a[@id="edit-cancel" and contains(@href, :path)]',
+ [':path' => '/examples/config-entity-example']
+ );
+ $this->assertEqual(count($cancel_button), 1, 'Found cancel button linking to list page.');
+ }
+
+ /**
+ * Wrap an assertion for the action button.
+ *
+ * @param string $path
+ * Drupal path to a page.
+ */
+ protected function assertActionButton($path) {
+ $button_element = $this->xpath(
+ '//a[contains(@class, "button-action") and contains(@data-drupal-link-system-path, :path)]',
+ [':path' => $path]
+ );
+ $this->assertEqual(count($button_element), 1, 'Found action button for path: ' . $path);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.info.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.info.yml
new file mode 100644
index 000000000..914f2720f
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.info.yml
@@ -0,0 +1,15 @@
+name: Content Entity Example
+type: module
+description: Demonstrates how to create a content entity.
+package: Example modules
+# core: 8.x
+# These modules are required by the tests, must be available at bootstrap time
+dependencies:
+ - drupal:options
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.action.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.action.yml
new file mode 100644
index 000000000..d1ead9d33
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.action.yml
@@ -0,0 +1,11 @@
+# All action links for this module
+
+content_entity_example.contact_add:
+ # Which route will be called by the link
+ route_name: content_entity_example.contact_add
+ title: 'Add contact'
+
+ # Where will the link appear, defined by route name.
+ appears_on:
+ - entity.content_entity_example_contact.collection
+ - entity.content_entity_example_contact.canonical
diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.menu.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.menu.yml
new file mode 100644
index 000000000..6454748ec
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.menu.yml
@@ -0,0 +1,12 @@
+# Define the menu links for this module
+
+entity.content_entity_example_contact.collection:
+ title: 'Content Entity Example: Contacts listing'
+ route_name: entity.content_entity_example_contact.collection
+ description: 'List contacts'
+ weight: 10
+content_entity_example_contact.admin.structure.settings:
+ title: 'Contact settings'
+ description: 'Configure contact entity'
+ route_name: content_entity_example.contact_settings
+ parent: system.admin_structure
diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.task.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.task.yml
new file mode 100644
index 000000000..a76cf585a
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.task.yml
@@ -0,0 +1,22 @@
+# Define the 'local' links for the module
+
+contact.settings_tab:
+ route_name: content_entity_example.contact_settings
+ title: Settings
+ base_route: content_entity_example.contact_settings
+
+contact.view:
+ route_name: entity.content_entity_example_contact.canonical
+ base_route: entity.content_entity_example_contact.canonical
+ title: View
+
+contact.page_edit:
+ route_name: entity.content_entity_example_contact.edit_form
+ base_route: entity.content_entity_example_contact.canonical
+ title: Edit
+
+contact.delete_confirm:
+ route_name: entity.content_entity_example_contact.delete_form
+ base_route: entity.content_entity_example_contact.canonical
+ title: Delete
+ weight: 10
diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.module b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.module
new file mode 100644
index 000000000..ff7e59a63
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.module
@@ -0,0 +1,45 @@
+. (replace dashes with underscores)
+ * Example: 'entity.content_entity_example_contact.canonical'
+ *
+ * See routing file above for the corresponding implementation
+ *
+ * The Contact class defines methods and fields for the contact entity.
+ *
+ * Being derived from the ContentEntityBase class, we can override the methods
+ * we want. In our case we want to provide access to the standard fields about
+ * creation and changed time stamps.
+ *
+ * Our interface (see ContactInterface) also exposes the EntityOwnerInterface.
+ * This allows us to provide methods for setting and providing ownership
+ * information.
+ *
+ * The most important part is the definitions of the field properties for this
+ * entity type. These are of the same type as fields added through the GUI, but
+ * they can by changed in code. In the definition we can define if the user with
+ * the rights privileges can influence the presentation (view, edit) of each
+ * field.
+ *
+ * The class also uses the EntityChangedTrait trait which allows it to record
+ * timestamps of save operations.
+ */
+class Contact extends ContentEntityBase implements ContactInterface {
+
+ use EntityChangedTrait;
+
+ /**
+ * {@inheritdoc}
+ *
+ * When a new entity instance is added, set the user_id entity reference to
+ * the current user as the creator of the instance.
+ */
+ public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
+ parent::preCreate($storage_controller, $values);
+ $values += [
+ 'user_id' => \Drupal::currentUser()->id(),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCreatedTime() {
+ return $this->get('created')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChangedTime() {
+ return $this->get('changed')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOwner() {
+ return $this->get('user_id')->entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOwnerId() {
+ return $this->get('user_id')->target_id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOwnerId($uid) {
+ $this->set('user_id', $uid);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOwner(UserInterface $account) {
+ $this->set('user_id', $account->id());
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Define the field properties here.
+ *
+ * Field name, type and size determine the table structure.
+ *
+ * In addition, we can define how the field and its content can be manipulated
+ * in the GUI. The behaviour of the widgets used can be determined here.
+ */
+ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+
+ // Standard field, used as unique if primary index.
+ $fields['id'] = BaseFieldDefinition::create('integer')
+ ->setLabel(t('ID'))
+ ->setDescription(t('The ID of the Contact entity.'))
+ ->setReadOnly(TRUE);
+
+ // Standard field, unique outside of the scope of the current project.
+ $fields['uuid'] = BaseFieldDefinition::create('uuid')
+ ->setLabel(t('UUID'))
+ ->setDescription(t('The UUID of the Contact entity.'))
+ ->setReadOnly(TRUE);
+
+ // Name field for the contact.
+ // We set display options for the view as well as the form.
+ // Users with correct privileges can change the view and edit configuration.
+ $fields['name'] = BaseFieldDefinition::create('string')
+ ->setLabel(t('Name'))
+ ->setDescription(t('The name of the Contact entity.'))
+ ->setSettings([
+ 'max_length' => 255,
+ 'text_processing' => 0,
+ ])
+ // Set no default value.
+ ->setDefaultValue(NULL)
+ ->setDisplayOptions('view', [
+ 'label' => 'above',
+ 'type' => 'string',
+ 'weight' => -6,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'string_textfield',
+ 'weight' => -6,
+ ])
+ ->setDisplayConfigurable('form', TRUE)
+ ->setDisplayConfigurable('view', TRUE);
+
+ $fields['first_name'] = BaseFieldDefinition::create('string')
+ ->setLabel(t('First Name'))
+ ->setDescription(t('The first name of the Contact entity.'))
+ ->setSettings([
+ 'max_length' => 255,
+ 'text_processing' => 0,
+ ])
+ // Set no default value.
+ ->setDefaultValue(NULL)
+ ->setDisplayOptions('view', [
+ 'label' => 'above',
+ 'type' => 'string',
+ 'weight' => -5,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'string_textfield',
+ 'weight' => -5,
+ ])
+ ->setDisplayConfigurable('form', TRUE)
+ ->setDisplayConfigurable('view', TRUE);
+
+ // Gender field for the contact.
+ // ListTextType with a drop down menu widget.
+ // The values shown in the menu are 'male' and 'female'.
+ // In the view the field content is shown as string.
+ // In the form the choices are presented as options list.
+ $fields['gender'] = BaseFieldDefinition::create('list_string')
+ ->setLabel(t('Gender'))
+ ->setDescription(t('The gender of the Contact entity.'))
+ ->setSettings([
+ 'allowed_values' => [
+ 'female' => 'female',
+ 'male' => 'male',
+ ],
+ ])
+ ->setDisplayOptions('view', [
+ 'label' => 'above',
+ 'type' => 'string',
+ 'weight' => -4,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'options_select',
+ 'weight' => -4,
+ ])
+ ->setDisplayConfigurable('form', TRUE)
+ ->setDisplayConfigurable('view', TRUE);
+
+ // Owner field of the contact.
+ // Entity reference field, holds the reference to the user object.
+ // The view shows the user name field of the user.
+ // The form presents a auto complete field for the user name.
+ $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
+ ->setLabel(t('User Name'))
+ ->setDescription(t('The Name of the associated user.'))
+ ->setSetting('target_type', 'user')
+ ->setSetting('handler', 'default')
+ ->setDisplayOptions('view', [
+ 'label' => 'above',
+ 'type' => 'author',
+ 'weight' => -3,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'entity_reference_autocomplete',
+ 'settings' => [
+ 'match_operator' => 'CONTAINS',
+ 'size' => 60,
+ 'placeholder' => '',
+ ],
+ 'weight' => -3,
+ ])
+ ->setDisplayConfigurable('form', TRUE)
+ ->setDisplayConfigurable('view', TRUE);
+
+ // Role field for the contact.
+ // The values shown in options are 'administrator' and 'user'.
+ $fields['role'] = BaseFieldDefinition::create('list_string')
+ ->setLabel(t('Role'))
+ ->setDescription(t('The role of the Contact entity.'))
+ ->setSettings([
+ 'allowed_values' => [
+ 'administrator' => 'administrator',
+ 'user' => 'user',
+ ],
+ ])
+ // Set the default value of this field to 'user'.
+ ->setDefaultValue('user')
+ ->setDisplayOptions('view', [
+ 'label' => 'above',
+ 'type' => 'string',
+ 'weight' => -2,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'options_select',
+ 'weight' => -2,
+ ])
+ ->setDisplayConfigurable('form', TRUE)
+ ->setDisplayConfigurable('view', TRUE);
+
+ $fields['langcode'] = BaseFieldDefinition::create('language')
+ ->setLabel(t('Language code'))
+ ->setDescription(t('The language code of ContentEntityExample entity.'));
+ $fields['created'] = BaseFieldDefinition::create('created')
+ ->setLabel(t('Created'))
+ ->setDescription(t('The time that the entity was created.'));
+
+ $fields['changed'] = BaseFieldDefinition::create('changed')
+ ->setLabel(t('Changed'))
+ ->setDescription(t('The time that the entity was last edited.'));
+
+ return $fields;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Entity/Controller/ContactListBuilder.php b/sites/all/modules/examples/examples/content_entity_example/src/Entity/Controller/ContactListBuilder.php
new file mode 100644
index 000000000..1b271938d
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/src/Entity/Controller/ContactListBuilder.php
@@ -0,0 +1,99 @@
+get('entity.manager')->getStorage($entity_type->id()),
+ $container->get('url_generator')
+ );
+ }
+
+ /**
+ * Constructs a new ContactListBuilder object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+ * The entity type definition.
+ * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+ * The entity storage class.
+ * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
+ * The url generator.
+ */
+ public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, UrlGeneratorInterface $url_generator) {
+ parent::__construct($entity_type, $storage);
+ $this->urlGenerator = $url_generator;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * We override ::render() so that we can add our own content above the table.
+ * parent::render() is where EntityListBuilder creates the table using our
+ * buildHeader() and buildRow() implementations.
+ */
+ public function render() {
+ $build['description'] = [
+ '#markup' => $this->t('Content Entity Example implements a Contacts model. These contacts are fieldable entities. You can manage the fields on the Contacts admin page.', [
+ '@adminlink' => $this->urlGenerator->generateFromRoute('content_entity_example.contact_settings'),
+ ]),
+ ];
+ $build['table'] = parent::render();
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Building the header and content lines for the contact list.
+ *
+ * Calling the parent::buildHeader() adds a column for the possible actions
+ * and inserts the 'edit' and 'delete' links as defined for the entity type.
+ */
+ public function buildHeader() {
+ $header['id'] = $this->t('ContactID');
+ $header['name'] = $this->t('Name');
+ $header['first_name'] = $this->t('First Name');
+ $header['gender'] = $this->t('Gender');
+ $header['role'] = $this->t('Role');
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ /* @var $entity \Drupal\content_entity_example\Entity\Contact */
+ $row['id'] = $entity->id();
+ $row['name'] = $entity->link();
+ $row['first_name'] = $entity->first_name->value;
+ $row['gender'] = $entity->gender->value;
+ $row['role'] = $entity->role->value;
+ return $row + parent::buildRow($entity);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactDeleteForm.php b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactDeleteForm.php
new file mode 100644
index 000000000..c224dc21b
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactDeleteForm.php
@@ -0,0 +1,56 @@
+t('Are you sure you want to delete entity %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * If the delete command is canceled, return to the contact list.
+ */
+ public function getCancelUrl() {
+ return new Url('entity.content_entity_example_contact.collection');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Delete the entity and log the event. logger() replaces the watchdog.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $entity = $this->getEntity();
+ $entity->delete();
+
+ $this->logger('content_entity_example')->notice('@type: deleted %title.',
+ [
+ '@type' => $this->entity->bundle(),
+ '%title' => $this->entity->label(),
+ ]);
+ $form_state->setRedirect('entity.content_entity_example_contact.collection');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactForm.php b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactForm.php
new file mode 100644
index 000000000..e35a4624c
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactForm.php
@@ -0,0 +1,42 @@
+entity;
+
+ $form['langcode'] = [
+ '#title' => $this->t('Language'),
+ '#type' => 'language_select',
+ '#default_value' => $entity->getUntranslated()->language()->getId(),
+ '#languages' => Language::STATE_ALL,
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ $form_state->setRedirect('entity.content_entity_example_contact.collection');
+ $entity = $this->getEntity();
+ $entity->save();
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactSettingsForm.php b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactSettingsForm.php
new file mode 100644
index 000000000..e44156c10
--- /dev/null
+++ b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactSettingsForm.php
@@ -0,0 +1,39 @@
+assertSession();
+
+ $web_user = $this->drupalCreateUser([
+ 'add contact entity',
+ 'edit contact entity',
+ 'view contact entity',
+ 'delete contact entity',
+ 'administer contact entity',
+ 'administer content_entity_example_contact display',
+ 'administer content_entity_example_contact fields',
+ 'administer content_entity_example_contact form display',
+ ]);
+
+ // Anonymous User should not see the link to the listing.
+ $assert->pageTextNotContains('Content Entity Example: Contacts Listing');
+
+ $this->drupalLogin($web_user);
+
+ // Web_user user has the right to view listing.
+ $assert->linkExists('Content Entity Example: Contacts Listing');
+
+ $this->clickLink('Content Entity Example: Contacts Listing');
+
+ // WebUser can add entity content.
+ $assert->linkExists('Add Contact');
+
+ $this->clickLink(t('Add Contact'));
+
+ $assert->fieldValueEquals('name[0][value]', '');
+ $assert->fieldValueEquals('name[0][value]', '');
+ $assert->fieldValueEquals('name[0][value]', '');
+ $assert->fieldValueEquals('name[0][value]', '');
+
+ $user_ref = $web_user->name->value . ' (' . $web_user->id() . ')';
+ $assert->fieldValueEquals('user_id[0][target_id]', $user_ref);
+
+ // Post content, save an instance. Go back to list after saving.
+ $edit = [
+ 'name[0][value]' => 'test name',
+ 'first_name[0][value]' => 'test first name',
+ 'gender' => 'male',
+ 'role' => 'administrator',
+ ];
+ $this->drupalPostForm(NULL, $edit, 'Save');
+
+ // Entity listed.
+ $assert->linkExists('Edit');
+ $assert->linkExists('Delete');
+
+ $this->clickLink('test name');
+
+ // Entity shown.
+ $assert->pageTextContains('test name');
+ $assert->pageTextContains('test first name');
+ $assert->pageTextContains('administrator');
+ $assert->pageTextContains('male');
+ $assert->linkExists('Add Contact');
+ $assert->linkExists('Edit');
+ $assert->linkExists('Delete');
+
+ // Delete the entity.
+ $this->clickLink('Delete');
+
+ // Confirm deletion.
+ $assert->linkExists('Cancel');
+ $this->drupalPostForm(NULL, [], 'Delete');
+
+ // Back to list, must be empty.
+ $assert->pageTextNotContains('test name');
+
+ // Settings page.
+ $this->drupalGet('admin/structure/content_entity_example_contact_settings');
+ $assert->pageTextContains('Contact Settings');
+
+ // Make sure the field manipulation links are available.
+ $assert->linkExists('Settings');
+ $assert->linkExists('Manage fields');
+ $assert->linkExists('Manage form display');
+ $assert->linkExists('Manage display');
+ }
+
+ /**
+ * Test all paths exposed by the module, by permission.
+ */
+ public function testPaths() {
+ $assert = $this->assertSession();
+
+ // Generate a contact so that we can test the paths against it.
+ $contact = Contact::create(
+ [
+ 'name' => 'somename',
+ 'first_name' => 'Joe',
+ 'gender' => 'female',
+ 'role' => 'administrator',
+ ]
+ );
+ $contact->save();
+
+ // Gather the test data.
+ $data = $this->providerTestPaths($contact->id());
+
+ // Run the tests.
+ foreach ($data as $datum) {
+ // drupalCreateUser() doesn't know what to do with an empty permission
+ // array, so we help it out.
+ if ($datum[2]) {
+ $user = $this->drupalCreateUser([$datum[2]]);
+ $this->drupalLogin($user);
+ }
+ else {
+ $user = $this->drupalCreateUser();
+ $this->drupalLogin($user);
+ }
+ $this->drupalGet($datum[1]);
+ $assert->statusCodeEquals($datum[0]);
+ }
+ }
+
+ /**
+ * Data provider for testPaths.
+ *
+ * @param int $contact_id
+ * The id of an existing Contact entity.
+ *
+ * @return array
+ * Nested array of testing data. Arranged like this:
+ * - Expected response code.
+ * - Path to request.
+ * - Permission for the user.
+ */
+ protected function providerTestPaths($contact_id) {
+ return [
+ [
+ 200,
+ '/content_entity_example_contact/' . $contact_id,
+ 'view contact entity',
+ ],
+ [
+ 403,
+ '/content_entity_example_contact/' . $contact_id,
+ '',
+ ],
+ [
+ 200,
+ '/content_entity_example_contact/list',
+ 'view contact entity',
+ ],
+ [
+ 403,
+ '/content_entity_example_contact/list',
+ '',
+ ],
+ [
+ 200,
+ '/content_entity_example_contact/add',
+ 'add contact entity',
+ ],
+ [
+ 403,
+ '/content_entity_example_contact/add',
+ '',
+ ],
+ [
+ 200,
+ '/content_entity_example_contact/' . $contact_id . '/edit',
+ 'edit contact entity',
+ ],
+ [
+ 403,
+ '/content_entity_example_contact/' . $contact_id . '/edit',
+ '',
+ ],
+ [
+ 200,
+ '/contact/' . $contact_id . '/delete',
+ 'delete contact entity',
+ ],
+ [
+ 403,
+ '/contact/' . $contact_id . '/delete',
+ '',
+ ],
+ [
+ 200,
+ 'admin/structure/content_entity_example_contact_settings',
+ 'administer contact entity',
+ ],
+ [
+ 403,
+ 'admin/structure/content_entity_example_contact_settings',
+ '',
+ ],
+ ];
+ }
+
+ /**
+ * Test add new fields to the contact entity.
+ */
+ public function testAddFields() {
+ $web_user = $this->drupalCreateUser([
+ 'administer contact entity',
+ 'administer content_entity_example_contact display',
+ 'administer content_entity_example_contact fields',
+ 'administer content_entity_example_contact form display',
+ ]);
+
+ $this->drupalLogin($web_user);
+ $entity_name = 'content_entity_example_contact';
+ $add_field_url = 'admin/structure/' . $entity_name . '_settings/fields/add-field';
+ $this->drupalGet($add_field_url);
+ $field_name = 'test_name';
+ $edit = [
+ 'new_storage_type' => 'list_string',
+ 'label' => 'test name',
+ 'field_name' => $field_name,
+ ];
+
+ $this->drupalPostForm(NULL, $edit, 'Save and continue');
+ $expected_path = $this->buildUrl('admin/structure/' . $entity_name . '_settings/fields/' . $entity_name . '.' . $entity_name . '.field_' . $field_name . '/storage');
+
+ // Fetch url without query parameters.
+ $current_path = strtok($this->getUrl(), '?');
+ $this->assertEquals($expected_path, $current_path);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/cron_example/config/install/cron_example.settings.yml b/sites/all/modules/examples/examples/cron_example/config/install/cron_example.settings.yml
new file mode 100644
index 000000000..8527484d1
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/config/install/cron_example.settings.yml
@@ -0,0 +1,4 @@
+# Default form values for \Drupal\cron_example\Form\CronExampleForm.
+# @see examples/cron_example/config/schema/cron_example.schema.yml
+# @see \Drupal\cron_example\Form\CronExampleForm::getEditableConfigNames()
+interval: 300
diff --git a/sites/all/modules/examples/examples/cron_example/config/schema/cron_example.schema.yml b/sites/all/modules/examples/examples/cron_example/config/schema/cron_example.schema.yml
new file mode 100644
index 000000000..f80c92cfc
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/config/schema/cron_example.schema.yml
@@ -0,0 +1,12 @@
+# Set configuration defaults. This schema describes the cron_example.settings
+# config. The defaults for this config are set in
+# config/install/cron_example.settings.yml and then used by
+# \Drupal\cron_example\Form\CronExampleForm.
+# @see https://www.drupal.org/node/1905070
+cron_example.settings:
+ type: config_object
+ label: 'Cron Example settings'
+ mapping:
+ interval:
+ type: integer
+ label: 'Period between cron runs'
diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.info.yml b/sites/all/modules/examples/examples/cron_example/cron_example.info.yml
new file mode 100644
index 000000000..812ad2b29
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/cron_example.info.yml
@@ -0,0 +1,14 @@
+name: Cron Example
+type: module
+description: 'Demonstrates hook_cron() and related features'
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.links.menu.yml b/sites/all/modules/examples/examples/cron_example/cron_example.links.menu.yml
new file mode 100644
index 000000000..faf80eb2d
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/cron_example.links.menu.yml
@@ -0,0 +1,3 @@
+cron_example.description:
+ title: Cron Example
+ route_name: cron_example
diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.module b/sites/all/modules/examples/examples/cron_example/cron_example.module
new file mode 100644
index 000000000..217c280cd
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/cron_example.module
@@ -0,0 +1,56 @@
+getEditable('cron_example.settings');
+ // Default to an hourly interval. Of course, cron has to be running at least
+ // hourly for this to work.
+ $interval = $cron_config->get('interval');
+ $interval = !empty($interval) ? $interval : 3600;
+
+ // We usually don't want to act every time cron runs (which could be every
+ // minute) so keep a time for the next run in the site state.
+ $next_execution = \Drupal::state()->get('cron_example.next_execution');
+ $next_execution = !empty($next_execution) ? $next_execution : 0;
+ if (REQUEST_TIME >= $next_execution) {
+ // This is a silly example of a cron job.
+ // It just makes it obvious that the job has run without
+ // making any changes to your database.
+ \Drupal::logger('cron_example')->notice('cron_example ran');
+ if (\Drupal::state()->get('cron_example_show_status_message')) {
+ drupal_set_message(t('cron_example executed at %time', ['%time' => date_iso8601(REQUEST_TIME)]));
+ \Drupal::state()->set('cron_example_show_status_message', FALSE);
+ }
+ \Drupal::state()->set('cron_example.next_execution', REQUEST_TIME + $interval);
+ }
+}
+
+/**
+ * @} End of "defgroup cron_example".
+ */
diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.routing.yml b/sites/all/modules/examples/examples/cron_example/cron_example.routing.yml
new file mode 100644
index 000000000..81afcf9ff
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/cron_example.routing.yml
@@ -0,0 +1,7 @@
+cron_example:
+ path: '/examples/cron-example'
+ defaults:
+ _form: '\Drupal\cron_example\Form\CronExampleForm'
+ _title: 'Cron Example'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/cron_example/src/Form/CronExampleForm.php b/sites/all/modules/examples/examples/cron_example/src/Form/CronExampleForm.php
new file mode 100644
index 000000000..8b70027cf
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/src/Form/CronExampleForm.php
@@ -0,0 +1,256 @@
+currentUser = $current_user;
+ $this->cron = $cron;
+ $this->queue = $queue;
+ $this->state = $state;
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('config.factory'),
+ $container->get('current_user'),
+ $container->get('cron'),
+ $container->get('queue'),
+ $container->get('state')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'cron_example';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $config = $this->configFactory->get('cron_example.settings');
+
+ $form['status'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Cron status information'),
+ '#open' => TRUE,
+ ];
+ $form['status']['intro'] = [
+ '#type' => 'item',
+ '#markup' => $this->t('The cron example demonstrates hook_cron() and hook_queue_info() processing. If you have administrative privileges you can run cron from this page and see the results.'),
+ ];
+
+ $next_execution = \Drupal::state()->get('cron_example.next_execution');
+ $next_execution = !empty($next_execution) ? $next_execution : REQUEST_TIME;
+
+ $args = [
+ '%time' => date_iso8601(\Drupal::state()->get('cron_example.next_execution')),
+ '%seconds' => $next_execution - REQUEST_TIME,
+ ];
+ $form['status']['last'] = [
+ '#type' => 'item',
+ '#markup' => $this->t('cron_example_cron() will next execute the first time cron runs after %time (%seconds seconds from now)', $args),
+ ];
+
+ if ($this->currentUser->hasPermission('administer site configuration')) {
+ $form['cron_run'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Run cron manually'),
+ '#open' => TRUE,
+ ];
+ $form['cron_run']['cron_reset'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t("Run cron_example's cron regardless of whether interval has expired."),
+ '#default_value' => FALSE,
+ ];
+ $form['cron_run']['cron_trigger']['actions'] = ['#type' => 'actions'];
+ $form['cron_run']['cron_trigger']['actions']['sumbit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Run cron now'),
+ '#submit' => [[$this, 'cronRun']],
+ ];
+ }
+
+ $form['cron_queue_setup'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Cron queue setup (for hook_cron_queue_info(), etc.)'),
+ '#open' => TRUE,
+ ];
+
+ $queue_1 = $this->queue->get('cron_example_queue_1');
+ $queue_2 = $this->queue->get('cron_example_queue_2');
+
+ $args = [
+ '%queue_1' => $queue_1->numberOfItems(),
+ '%queue_2' => $queue_2->numberOfItems(),
+ ];
+ $form['cron_queue_setup']['current_cron_queue_status'] = [
+ '#type' => 'item',
+ '#markup' => $this->t('There are currently %queue_1 items in queue 1 and %queue_2 items in queue 2', $args),
+ ];
+ $form['cron_queue_setup']['num_items'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Number of items to add to queue'),
+ '#options' => array_combine([1, 5, 10, 100, 1000], [1, 5, 10, 100, 1000]),
+ '#default_value' => 5,
+ ];
+ $form['cron_queue_setup']['queue'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Queue to add items to'),
+ '#options' => [
+ 'cron_example_queue_1' => $this->t('Queue 1'),
+ 'cron_example_queue_2' => $this->t('Queue 2'),
+ ],
+ '#default_value' => 'cron_example_queue_1',
+ ];
+ $form['cron_queue_setup']['actions'] = ['#type' => 'actions'];
+ $form['cron_queue_setup']['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Add jobs to queue'),
+ '#submit' => [[$this, 'addItems']],
+ ];
+
+ $form['configuration'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Configuration of cron_example_cron()'),
+ '#open' => TRUE,
+ ];
+ $form['configuration']['cron_example_interval'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Cron interval'),
+ '#description' => $this->t('Time after which cron_example_cron will respond to a processing request.'),
+ '#default_value' => $config->get('interval'),
+ '#options' => [
+ 60 => $this->t('1 minute'),
+ 300 => $this->t('5 minutes'),
+ 3600 => $this->t('1 hour'),
+ 86400 => $this->t('1 day'),
+ ],
+ ];
+
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * Allow user to directly execute cron, optionally forcing it.
+ */
+ public function cronRun(array &$form, FormStateInterface &$form_state) {
+ $config = $this->configFactory->getEditable('cron_example.settings');
+
+ $cron_reset = $form_state->getValue('cron_reset');
+ if (!empty($cron_reset)) {
+ \Drupal::state()->set('cron_example.next_execution', 0);
+ }
+
+ // Use a state variable to signal that cron was run manually from this form.
+ $this->state->set('cron_example_show_status_message', TRUE);
+ if ($this->cron->run()) {
+ drupal_set_message($this->t('Cron ran successfully.'));
+ }
+ else {
+ drupal_set_message($this->t('Cron run failed.'), 'error');
+ }
+ }
+
+ /**
+ * Add the items to the queue when signaled by the form.
+ */
+ public function addItems(array &$form, FormStateInterface &$form_state) {
+ $values = $form_state->getValues();
+ $queue_name = $form['cron_queue_setup']['queue'][$values['queue']]['#title'];
+ $num_items = $form_state->getValue('num_items');
+ // Queues are defined by a QueueWorker Plugin which are selected by their
+ // id attritbute.
+ // @see \Drupal\cron_example\Plugin\QueueWorker\ReportWorkerOne
+ $queue = $this->queue->get($values['queue']);
+
+ for ($i = 1; $i <= $num_items; $i++) {
+ // Create a new item, a new data object, which is passed to the
+ // QueueWorker's processItem() method.
+ $item = new \stdClass();
+ $item->created = REQUEST_TIME;
+ $item->sequence = $i;
+ $queue->createItem($item);
+ }
+
+ $args = [
+ '%num' => $num_items,
+ '%queue' => $queue_name,
+ ];
+ drupal_set_message($this->t('Added %num items to %queue', $args));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Update the interval as stored in configuration. This will be read when
+ // this modules hook_cron function fires and will be used to ensure that
+ // action is taken only after the appropiate time has elapsed.
+ $this->configFactory->getEditable('cron_example.settings')
+ ->set('interval', $form_state->getValue('cron_example_interval'))
+ ->save();
+
+ parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEditableConfigNames() {
+ return ['cron_example.settings'];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerBase.php b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerBase.php
new file mode 100644
index 000000000..7377b7dce
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerBase.php
@@ -0,0 +1,92 @@
+state = $state;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('state'),
+ $container->get('logger.factory')
+ );
+ }
+
+ /**
+ * Simple reporter log and display information about the queue.
+ *
+ * @param int $worker
+ * Worker number.
+ * @param object $item
+ * The $item which was stored in the cron queue.
+ */
+ protected function reportWork($worker, $item) {
+ if ($this->state->get('cron_example_show_status_message')) {
+ drupal_set_message(
+ $this->t('Queue @worker worker processed item with sequence @sequence created at @time', [
+ '@worker' => $worker,
+ '@sequence' => $item->sequence,
+ '@time' => date_iso8601($item->created),
+ ])
+ );
+ }
+ $this->logger->get('cron_example')->info('Queue @worker worker processed item with sequence @sequence created at @time', [
+ '@worker' => $worker,
+ '@sequence' => $item->sequence,
+ '@time' => date_iso8601($item->created),
+ ]);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerOne.php b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerOne.php
new file mode 100644
index 000000000..653223bd7
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerOne.php
@@ -0,0 +1,29 @@
+reportWork(1, $data);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerTwo.php b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerTwo.php
new file mode 100644
index 000000000..b4fad9a2f
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerTwo.php
@@ -0,0 +1,29 @@
+reportWork(2, $data);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/cron_example/tests/src/Functional/CronExampleTest.php b/sites/all/modules/examples/examples/cron_example/tests/src/Functional/CronExampleTest.php
new file mode 100644
index 000000000..c3b07164c
--- /dev/null
+++ b/sites/all/modules/examples/examples/cron_example/tests/src/Functional/CronExampleTest.php
@@ -0,0 +1,90 @@
+drupalLogin($this->drupalCreateUser(['administer site configuration', 'access content']));
+
+ $this->cronConfig = \Drupal::configFactory()->getEditable('cron_example.settings');
+ }
+
+ /**
+ * Create an example node, test block through admin and user interfaces.
+ */
+ public function testCronExampleBasic() {
+ $assert = $this->assertSession();
+
+ // Pretend that cron has never been run (even though simpletest seems to
+ // run it once...).
+ \Drupal::state()->set('cron_example.next_execution', 0);
+ $this->drupalGet('examples/cron-example');
+
+ // Initial run should cause cron_example_cron() to fire.
+ $post = [];
+ $this->drupalPostForm('examples/cron-example', $post, 'Run cron now');
+ $assert->pageTextContains('cron_example executed at');
+
+ // Forcing should also cause cron_example_cron() to fire.
+ $post['cron_reset'] = TRUE;
+ $this->drupalPostForm(NULL, $post, 'Run cron now');
+ $assert->pageTextContains('cron_example executed at');
+
+ // But if followed immediately and not forced, it should not fire.
+ $post['cron_reset'] = FALSE;
+ $this->drupalPostForm(NULL, $post, 'Run cron now');
+ $assert->statusCodeEquals(200);
+ $assert->pageTextNotContains('cron_example executed at');
+ $assert->pageTextContains('There are currently 0 items in queue 1 and 0 items in queue 2');
+
+ $post = [
+ 'num_items' => 5,
+ 'queue' => 'cron_example_queue_1',
+ ];
+ $this->drupalPostForm(NULL, $post, 'Add jobs to queue');
+ $assert->pageTextContains('There are currently 5 items in queue 1 and 0 items in queue 2');
+
+ $post = [
+ 'num_items' => 100,
+ 'queue' => 'cron_example_queue_2',
+ ];
+ $this->drupalPostForm(NULL, $post, 'Add jobs to queue');
+ $assert->pageTextContains('There are currently 5 items in queue 1 and 100 items in queue 2');
+
+ $this->drupalPostForm('examples/cron-example', [], 'Run cron now');
+ $assert->responseMatches('/Queue 1 worker processed item with sequence 5 /');
+ $assert->responseMatches('/Queue 2 worker processed item with sequence 100 /');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/css/examples.icons.css b/sites/all/modules/examples/examples/css/examples.icons.css
new file mode 100644
index 000000000..da9645465
--- /dev/null
+++ b/sites/all/modules/examples/examples/css/examples.icons.css
@@ -0,0 +1,13 @@
+/**
+ * @file
+ * Styling for the user module icons.
+ */
+
+/**
+ * Toolbar tab icon.
+ */
+.toolbar-bar .toolbar-icon-examples:before,
+.toolbar-bar .toolbar-icon-examples:active:before,
+.toolbar-bar .toolbar-icon-examples.active:before {
+ background-image: url(../images/examples.svg);
+}
diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.info.yml b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.info.yml
new file mode 100644
index 000000000..57858f94d
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.info.yml
@@ -0,0 +1,15 @@
+name: DBTNG Example
+type: module
+description: 'Demonstrates how to use the database API: DBTNG.'
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - drupal:user
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.install b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.install
new file mode 100644
index 000000000..a1c729814
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.install
@@ -0,0 +1,95 @@
+ 'John',
+ 'surname' => 'Doe',
+ 'age' => 0,
+ ];
+ db_insert('dbtng_example')
+ ->fields($fields)
+ ->execute();
+
+ // Add another entry.
+ $fields = [
+ 'name' => 'John',
+ 'surname' => 'Roe',
+ 'age' => 100,
+ 'uid' => 1,
+ ];
+ db_insert('dbtng_example')
+ ->fields($fields)
+ ->execute();
+}
+
+/**
+ * Implements hook_schema().
+ *
+ * Defines the database tables used by this module.
+ *
+ * @see hook_schema()
+ *
+ * @ingroup dbtng_example
+ */
+function dbtng_example_schema() {
+ $schema['dbtng_example'] = [
+ 'description' => 'Stores example person entries for demonstration purposes.',
+ 'fields' => [
+ 'pid' => [
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique person ID.',
+ ],
+ 'uid' => [
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "Creator user's {users}.uid",
+ ],
+ 'name' => [
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Name of the person.',
+ ],
+ 'surname' => [
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Surname of the person.',
+ ],
+ 'age' => [
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'tiny',
+ 'description' => 'The age of the person in years.',
+ ],
+ ],
+ 'primary key' => ['pid'],
+ 'indexes' => [
+ 'name' => ['name'],
+ 'surname' => ['surname'],
+ 'age' => ['age'],
+ ],
+ ];
+
+ return $schema;
+}
diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.links.menu.yml b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.links.menu.yml
new file mode 100644
index 000000000..92270d8c2
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.links.menu.yml
@@ -0,0 +1,25 @@
+dbtng_example.description:
+ title: DBTNG Example
+ route_name: dbtng_example
+ expanded: TRUE
+
+dbtng_example.list:
+ title: List
+ route_name: dbtng_list
+ weight: 0
+ parent: dbtng_example.description
+dbtng_example.add:
+ title: Add entry
+ route_name: dbtng_add
+ weight: 1
+ parent: dbtng_example.description
+dbtng_example.update:
+ title: Update entry
+ route_name: dbtng_update
+ weight: 2
+ parent: dbtng_example.description
+dbtng_example.advanced:
+ title: Advanced list
+ route_name: dbtng_advanced
+ weight: 3
+ parent: dbtng_example.description
diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.module b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.module
new file mode 100644
index 000000000..070ee5b5f
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.module
@@ -0,0 +1,63 @@
+fields(array('name' => 'John', 'surname' => 'Doe'))
+ * ->execute();
+ * @endcode
+ *
+ * db_update() example:
+ * @code
+ * // UPDATE {dbtng_example} SET name = 'Jane' WHERE name = 'John'
+ * db_update('dbtng_example')
+ * ->fields(array('name' => 'Jane'))
+ * ->condition('name', 'John')
+ * ->execute();
+ * @endcode
+ *
+ * db_delete() example:
+ * @code
+ * // DELETE FROM {dbtng_example} WHERE name = 'Jane'
+ * db_delete('dbtng_example')
+ * ->condition('name', 'Jane')
+ * ->execute();
+ * @endcode
+ *
+ * See @link database Database Abstraction Layer @endlink
+ * @see db_insert()
+ * @see db_update()
+ * @see db_delete()
+ */
+
+/**
+ * @} End of "defgroup dbtng_example".
+ */
diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.routing.yml b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.routing.yml
new file mode 100644
index 000000000..d301a8138
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.routing.yml
@@ -0,0 +1,39 @@
+dbtng_example:
+ path: 'examples/dbtng-example'
+ defaults:
+ _title: 'DBTNG Example'
+ _controller: '\Drupal\dbtng_example\Controller\DbtngExampleController::entryList'
+ requirements:
+ _permission: 'access content'
+
+dbtng_list:
+ path: 'examples/dbtng-example/list'
+ defaults:
+ _title: 'List'
+ _controller: '\Drupal\dbtng_example\Controller\DbtngExampleController::entryList'
+ requirements:
+ _permission: 'access content'
+
+dbtng_add:
+ path: 'examples/dbtng-example/add'
+ defaults:
+ _title: 'Add entry'
+ _form: '\Drupal\dbtng_example\Form\DbtngExampleAddForm'
+ requirements:
+ _permission: 'access content'
+
+dbtng_update:
+ path: 'examples/dbtng-example/update'
+ defaults:
+ _title: 'Update entry'
+ _form: '\Drupal\dbtng_example\Form\DbtngExampleUpdateForm'
+ requirements:
+ _permission: 'access content'
+
+dbtng_advanced:
+ path: 'examples/dbtng-example/advanced'
+ defaults:
+ _title: 'Advanced list'
+ _controller: '\Drupal\dbtng_example\Controller\DbtngExampleController::entryAdvancedList'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/dbtng_example/src/Controller/DbtngExampleController.php b/sites/all/modules/examples/examples/dbtng_example/src/Controller/DbtngExampleController.php
new file mode 100644
index 000000000..04f026b6e
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/src/Controller/DbtngExampleController.php
@@ -0,0 +1,78 @@
+ $this->t('Generate a list of all entries in the database. There is no filter in the query.'),
+ ];
+
+ $rows = [];
+ $headers = [t('Id'), t('uid'), t('Name'), t('Surname'), t('Age')];
+
+ foreach ($entries = DbtngExampleStorage::load() as $entry) {
+ // Sanitize each entry.
+ $rows[] = array_map('Drupal\Component\Utility\SafeMarkup::checkPlain', (array) $entry);
+ }
+ $content['table'] = [
+ '#type' => 'table',
+ '#header' => $headers,
+ '#rows' => $rows,
+ '#empty' => t('No entries available.'),
+ ];
+ // Don't cache this page.
+ $content['#cache']['max-age'] = 0;
+
+ return $content;
+ }
+
+ /**
+ * Render a filtered list of entries in the database.
+ */
+ public function entryAdvancedList() {
+ $content = [];
+
+ $content['message'] = [
+ '#markup' => $this->t('A more complex list of entries in the database.') . ' ' .
+ $this->t('Only the entries with name = "John" and age older than 18 years are shown, the username of the person who created the entry is also shown.'),
+ ];
+
+ $headers = [
+ t('Id'),
+ t('Created by'),
+ t('Name'),
+ t('Surname'),
+ t('Age'),
+ ];
+
+ $rows = [];
+ foreach ($entries = DbtngExampleStorage::advancedLoad() as $entry) {
+ // Sanitize each entry.
+ $rows[] = array_map('Drupal\Component\Utility\SafeMarkup::checkPlain', $entry);
+ }
+ $content['table'] = [
+ '#type' => 'table',
+ '#header' => $headers,
+ '#rows' => $rows,
+ '#attributes' => ['id' => 'dbtng-example-advanced-list'],
+ '#empty' => t('No entries available.'),
+ ];
+ // Don't cache this page.
+ $content['#cache']['max-age'] = 0;
+ return $content;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/dbtng_example/src/DbtngExampleStorage.php b/sites/all/modules/examples/examples/dbtng_example/src/DbtngExampleStorage.php
new file mode 100644
index 000000000..eea0a69d6
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/src/DbtngExampleStorage.php
@@ -0,0 +1,222 @@
+fields($entry)
+ ->execute();
+ }
+ catch (\Exception $e) {
+ drupal_set_message(t('db_insert failed. Message = %message, query= %query', [
+ '%message' => $e->getMessage(),
+ '%query' => $e->query_string,
+ ]
+ ), 'error');
+ }
+ return $return_value;
+ }
+
+ /**
+ * Update an entry in the database.
+ *
+ * @param array $entry
+ * An array containing all the fields of the item to be updated.
+ *
+ * @return int
+ * The number of updated rows.
+ *
+ * @see db_update()
+ */
+ public static function update(array $entry) {
+ try {
+ // db_update()...->execute() returns the number of rows updated.
+ $count = db_update('dbtng_example')
+ ->fields($entry)
+ ->condition('pid', $entry['pid'])
+ ->execute();
+ }
+ catch (\Exception $e) {
+ drupal_set_message(t('db_update failed. Message = %message, query= %query', [
+ '%message' => $e->getMessage(),
+ '%query' => $e->query_string,
+ ]
+ ), 'error');
+ }
+ return $count;
+ }
+
+ /**
+ * Delete an entry from the database.
+ *
+ * @param array $entry
+ * An array containing at least the person identifier 'pid' element of the
+ * entry to delete.
+ *
+ * @see db_delete()
+ */
+ public static function delete(array $entry) {
+ db_delete('dbtng_example')
+ ->condition('pid', $entry['pid'])
+ ->execute();
+ }
+
+ /**
+ * Read from the database using a filter array.
+ *
+ * The standard function to perform reads was db_query(), and for static
+ * queries, it still is.
+ *
+ * db_query() used an SQL query with placeholders and arguments as parameters.
+ *
+ * Drupal DBTNG provides an abstracted interface that will work with a wide
+ * variety of database engines.
+ *
+ * db_query() is deprecated except when doing a static query. The following is
+ * perfectly acceptable in Drupal 8. See
+ * @link http://drupal.org/node/310072 the handbook page on static queries @endlink
+ *
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * db_query(
+ * "SELECT * FROM {dbtng_example} WHERE uid = :uid and name = :name",
+ * array(':uid' => 0, ':name' => 'John')
+ * )->execute();
+ * @endcode
+ *
+ * But for more dynamic queries, Drupal provides the db_select()
+ * API method, so there are several ways to perform the same SQL query. See
+ * the
+ * @link http://drupal.org/node/310075 handbook page on dynamic queries. @endlink
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->condition('uid', 0)
+ * ->condition('name', 'John')
+ * ->execute();
+ * @endcode
+ *
+ * Here is db_select with named placeholders:
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
+ * $arguments = array(':name' => 'John', ':uid' => 0);
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->where('uid = :uid AND name = :name', $arguments)
+ * ->execute();
+ * @endcode
+ *
+ * Conditions are stacked and evaluated as AND and OR depending on the type of
+ * query. For more information, read the conditional queries handbook page at:
+ * http://drupal.org/node/310086
+ *
+ * The condition argument is an 'equal' evaluation by default, but this can be
+ * altered:
+ * @code
+ * // SELECT * FROM {dbtng_example} WHERE age > 18
+ * db_select('dbtng_example')
+ * ->fields('dbtng_example')
+ * ->condition('age', 18, '>')
+ * ->execute();
+ * @endcode
+ *
+ * @param array $entry
+ * An array containing all the fields used to search the entries in the
+ * table.
+ *
+ * @return object
+ * An object containing the loaded entries if found.
+ *
+ * @see db_select()
+ * @see db_query()
+ * @see http://drupal.org/node/310072
+ * @see http://drupal.org/node/310075
+ */
+ public static function load(array $entry = []) {
+ // Read all fields from the dbtng_example table.
+ $select = db_select('dbtng_example', 'example');
+ $select->fields('example');
+
+ // Add each field and value as a condition to this query.
+ foreach ($entry as $field => $value) {
+ $select->condition($field, $value);
+ }
+ // Return the result in object format.
+ return $select->execute()->fetchAll();
+ }
+
+ /**
+ * Load dbtng_example records joined with user records.
+ *
+ * DBTNG also helps processing queries that return several rows, providing the
+ * found objects in the same query execution call.
+ *
+ * This function queries the database using a JOIN between users table and the
+ * example entries, to provide the username that created the entry, and
+ * creates a table with the results, processing each row.
+ *
+ * SELECT
+ * e.pid as pid, e.name as name, e.surname as surname, e.age as age
+ * u.name as username
+ * FROM
+ * {dbtng_example} e
+ * JOIN
+ * users u ON e.uid = u.uid
+ * WHERE
+ * e.name = 'John' AND e.age > 18
+ *
+ * @see db_select()
+ * @see http://drupal.org/node/310075
+ */
+ public static function advancedLoad() {
+ $select = db_select('dbtng_example', 'e');
+ // Join the users table, so we can get the entry creator's username.
+ $select->join('users_field_data', 'u', 'e.uid = u.uid');
+ // Select these specific fields for the output.
+ $select->addField('e', 'pid');
+ $select->addField('u', 'name', 'username');
+ $select->addField('e', 'name');
+ $select->addField('e', 'surname');
+ $select->addField('e', 'age');
+ // Filter only persons named "John".
+ $select->condition('e.name', 'John');
+ // Filter only persons older than 18 years.
+ $select->condition('e.age', 18, '>');
+ // Make sure we only get items 0-49, for scalability reasons.
+ $select->range(0, 50);
+
+ $entries = $select->execute()->fetchAll(\PDO::FETCH_ASSOC);
+
+ return $entries;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleAddForm.php b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleAddForm.php
new file mode 100644
index 000000000..04f5a839d
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleAddForm.php
@@ -0,0 +1,130 @@
+get('current_user')
+ );
+ // The StringTranslationTrait trait manages the string translation service
+ // for us. We can inject the service here.
+ $form->setStringTranslation($container->get('string_translation'));
+ return $form;
+ }
+
+ /**
+ * Construct the new form object.
+ */
+ public function __construct(AccountProxyInterface $current_user) {
+ $this->currentUser = $current_user;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'dbtng_add_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form = [];
+
+ $form['message'] = [
+ '#markup' => $this->t('Add an entry to the dbtng_example table.'),
+ ];
+
+ $form['add'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Add a person entry'),
+ ];
+ $form['add']['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Name'),
+ '#size' => 15,
+ ];
+ $form['add']['surname'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Surname'),
+ '#size' => 15,
+ ];
+ $form['add']['age'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Age'),
+ '#size' => 5,
+ '#description' => $this->t("Values greater than 127 will cause an exception. Try it - it's a great example why exception handling is needed with DTBNG."),
+ ];
+ $form['add']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Add'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ // Verify that the user is logged-in.
+ if ($this->currentUser->isAnonymous()) {
+ $form_state->setError($form['add'], $this->t('You must be logged in to add values to the database.'));
+ }
+ // Confirm that age is numeric.
+ if (!intval($form_state->getValue('age'))) {
+ $form_state->setErrorByName('age', $this->t('Age needs to be a number'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Gather the current user so the new record has ownership.
+ $account = $this->currentUser;
+ // Save the submitted entry.
+ $entry = [
+ 'name' => $form_state->getValue('name'),
+ 'surname' => $form_state->getValue('surname'),
+ 'age' => $form_state->getValue('age'),
+ 'uid' => $account->id(),
+ ];
+ $return = DbtngExampleStorage::insert($entry);
+ if ($return) {
+ drupal_set_message($this->t('Created entry @entry', ['@entry' => print_r($entry, TRUE)]));
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleUpdateForm.php b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleUpdateForm.php
new file mode 100644
index 000000000..8226bd95f
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleUpdateForm.php
@@ -0,0 +1,153 @@
+ '
',
+ '#suffix' => '
',
+ ];
+ // Add some explanatory text to the form.
+ $form['message'] = [
+ '#markup' => $this->t('Demonstrates a database update operation.'),
+ ];
+ // Query for items to display.
+ $entries = DbtngExampleStorage::load();
+ // Tell the user if there is nothing to display.
+ if (empty($entries)) {
+ $form['no_values'] = [
+ '#value' => t('No entries exist in the table dbtng_example table.'),
+ ];
+ return $form;
+ }
+
+ $keyed_entries = [];
+ foreach ($entries as $entry) {
+ $options[$entry->pid] = t('@pid: @name @surname (@age)', [
+ '@pid' => $entry->pid,
+ '@name' => $entry->name,
+ '@surname' => $entry->surname,
+ '@age' => $entry->age,
+ ]);
+ $keyed_entries[$entry->pid] = $entry;
+ }
+
+ // Grab the pid.
+ $pid = $form_state->getValue('pid');
+ // Use the pid to set the default entry for updating.
+ $default_entry = !empty($pid) ? $keyed_entries[$pid] : $entries[0];
+
+ // Save the entries into the $form_state. We do this so the AJAX callback
+ // doesn't need to repeat the query.
+ $form_state->setValue('entries', $keyed_entries);
+
+ $form['pid'] = [
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => t('Choose entry to update'),
+ '#default_value' => $default_entry->pid,
+ '#ajax' => [
+ 'wrapper' => 'updateform',
+ 'callback' => [$this, 'updateCallback'],
+ ],
+ ];
+
+ $form['name'] = [
+ '#type' => 'textfield',
+ '#title' => t('Updated first name'),
+ '#size' => 15,
+ '#default_value' => $default_entry->name,
+ ];
+
+ $form['surname'] = [
+ '#type' => 'textfield',
+ '#title' => t('Updated last name'),
+ '#size' => 15,
+ '#default_value' => $default_entry->surname,
+ ];
+ $form['age'] = [
+ '#type' => 'textfield',
+ '#title' => t('Updated age'),
+ '#size' => 4,
+ '#default_value' => $default_entry->age,
+ '#description' => t('Values greater than 127 will cause an exception'),
+ ];
+
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => t('Update'),
+ ];
+ return $form;
+ }
+
+ /**
+ * AJAX callback handler for the pid select.
+ *
+ * When the pid changes, populates the defaults from the database in the form.
+ */
+ public function updateCallback(array $form, FormStateInterface $form_state) {
+ // Gather the DB results from $form_state.
+ $entries = $form_state->getValue('entries');
+ // Use the specific entry for this $form_state.
+ $entry = $entries[$form_state->getValue('pid')];
+ // Setting the #value of items is the only way I was able to figure out
+ // to get replaced defaults on these items. #default_value will not do it
+ // and shouldn't.
+ foreach (['name', 'surname', 'age'] as $item) {
+ $form[$item]['#value'] = $entry->$item;
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ // Confirm that age is numeric.
+ if (!intval($form_state->getValue('age'))) {
+ $form_state->setErrorByName('age', t('Age needs to be a number'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Gather the current user so the new record has ownership.
+ $account = $this->currentUser();
+ // Save the submitted entry.
+ $entry = [
+ 'pid' => $form_state->getValue('pid'),
+ 'name' => $form_state->getValue('name'),
+ 'surname' => $form_state->getValue('surname'),
+ 'age' => $form_state->getValue('age'),
+ 'uid' => $account->id(),
+ ];
+ $count = DbtngExampleStorage::update($entry);
+ drupal_set_message(t('Updated entry @entry (@count row updated)', [
+ '@count' => $count,
+ '@entry' => print_r($entry, TRUE),
+ ]));
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/dbtng_example/tests/src/Functional/DbtngExampleTest.php b/sites/all/modules/examples/examples/dbtng_example/tests/src/Functional/DbtngExampleTest.php
new file mode 100644
index 000000000..5fd591a2b
--- /dev/null
+++ b/sites/all/modules/examples/examples/dbtng_example/tests/src/Functional/DbtngExampleTest.php
@@ -0,0 +1,222 @@
+assertSession();
+
+ // Assert that two entries were inserted at install.
+ $result = DbtngExampleStorage::load();
+ $this->assertCount(2, $result, 'Did not find two entries in the table after installing the module.');
+
+ // Test the example description page.
+ $this->drupalGet('/examples/dbtng-example');
+ $assert->statusCodeEquals(200);
+
+ // Verify and validate that default menu links were loaded for this module.
+ $links = $this->providerMenuLinks();
+ foreach ($links as $page => $hrefs) {
+ foreach ($hrefs as $href) {
+ $this->drupalGet($page);
+ $assert->linkByHrefExists($href);
+ }
+ }
+ }
+
+ /**
+ * Data provider for testing menu links.
+ *
+ * @return array
+ * Array of page -> link relationships to check for:
+ * - The key is the path to the page where our link should appear.
+ * - The value is an array of links that should appear on that page.
+ */
+ protected function providerMenuLinks() {
+ return [
+ '' => [
+ '/examples/dbtng-example',
+ ],
+ '/examples/dbtng-example' => [
+ '/examples/dbtng-example/add',
+ '/examples/dbtng-example/update',
+ '/examples/dbtng-example/advanced',
+ ],
+ ];
+ }
+
+ /**
+ * Test the UI.
+ */
+ public function testUi() {
+ $assert = $this->assertSession();
+
+ $this->drupalLogin($this->createUser());
+ // Test the basic list.
+ $this->drupalGet('/examples/dbtng-example');
+ $assert->statusCodeEquals(200);
+ $assert->pageTextMatches('%John[td/<>\w\s]+Doe%');
+
+ // Test the add tab.
+ // Add the new entry.
+ $this->drupalPostForm(
+ '/examples/dbtng-example/add',
+ [
+ 'name' => 'Some',
+ 'surname' => 'Anonymous',
+ 'age' => 33,
+ ],
+ 'Add'
+ );
+ // Now find the new entry.
+ $this->drupalGet('/examples/dbtng-example');
+ $assert->pageTextMatches('%Some[td/<>\w\s]+Anonymous%');
+ // Try the update tab.
+ // Find out the pid of our "anonymous" guy.
+ $result = DbtngExampleStorage::load(['surname' => 'Anonymous']);
+ $this->drupalGet('/examples/dbtng-example');
+ $this->assertCount(1, $result, 'Did not find one entry in the table with surname = "Anonymous".');
+ $entry = $result[0];
+ unset($entry->uid);
+
+ $entry = ['name' => 'NewFirstName', 'age' => 22];
+ $this->drupalPostForm('/examples/dbtng-example/update', $entry, 'Update');
+ // Now find the new entry.
+ $this->drupalGet('/examples/dbtng-example');
+ $assert->pageTextMatches('%NewFirstName[td/<>\w\s]+Anonymous%');
+
+ // Try the advanced tab.
+ $this->drupalGet('/examples/dbtng-example/advanced');
+ $rows = $this->xpath("//*[@id='dbtng-example-advanced-list'][1]/tbody/tr");
+ $this->assertCount(1, $rows);
+
+ $field = $this->xpath("//*[@id='dbtng-example-advanced-list'][1]/tbody/tr/td[4]");
+ $this->assertEquals('Roe', $field[0]->getText());
+
+ // Try to add an entry while logged out.
+ $this->drupalLogout();
+ $this->drupalPostForm(
+ '/examples/dbtng-example/add',
+ [
+ 'name' => 'Anonymous',
+ 'surname' => 'UserCannotPost',
+ 'age' => 'not a number',
+ ],
+ 'Add'
+ );
+ $assert->pageTextContains('You must be logged in to add values to the database.');
+ $assert->pageTextContains('Age needs to be a number');
+ }
+
+ /**
+ * Tests several combinations, adding entries, updating and deleting.
+ */
+ public function testDbtngExampleStorage() {
+ // Create a new entry.
+ $entry = [
+ 'name' => 'James',
+ 'surname' => 'Doe',
+ 'age' => 23,
+ ];
+ DbtngExampleStorage::insert($entry);
+
+ // Save another entry.
+ $entry = [
+ 'name' => 'Jane',
+ 'surname' => 'NotDoe',
+ 'age' => 19,
+ ];
+ DbtngExampleStorage::insert($entry);
+
+ // Verify that 4 records are found in the database.
+ $result = DbtngExampleStorage::load();
+ $this->assertCount(4, $result);
+
+ // Verify 2 of these records have 'Doe' as surname.
+ $result = DbtngExampleStorage::load(['surname' => 'Doe']);
+ $this->assertCount(2, $result, 'Did not find two entries in the table with surname = "Doe".');
+
+ // Now find our not-Doe entry.
+ $result = DbtngExampleStorage::load(['surname' => 'NotDoe']);
+ // Found one entry in the table with surname "NotDoe'.
+ $this->assertCount(1, $result, 'Did not find one entry in the table with surname "NotDoe');
+ // Our NotDoe will be changed to "NowDoe".
+ $entry = $result[0];
+ $entry->surname = "NowDoe";
+ // update() returns the number of entries updated.
+ $this->assertNotEquals(DbtngExampleStorage::update((array) $entry), 0);
+
+ $result = DbtngExampleStorage::load(['surname' => 'NowDoe']);
+ $this->assertCount(1, $result, "Did not find renamed 'NowDoe' surname.");
+
+ // Read only John Doe entry.
+ $result = DbtngExampleStorage::load(['name' => 'John', 'surname' => 'Doe']);
+ $this->assertCount(1, $result, 'Did not find one entry for John Doe.');
+
+ // Get the entry.
+ $entry = (array) end($result);
+ // Change age to 45.
+ $entry['age'] = 45;
+ // Update entry in database.
+ DbtngExampleStorage::update((array) $entry);
+
+ // Find entries with age = 45.
+ // Read only John Doe entry.
+ $result = DbtngExampleStorage::load(['surname' => 'NowDoe']);
+ // Found one entry with surname = Nowdoe.
+ $this->assertCount(1, $result, 'Did not find one entry with surname = Nowdoe.');
+
+ // Verify it is Jane NowDoe.
+ $entry = (array) end($result);
+ // The name Jane is found in the entry.
+ $this->assertEquals('Jane', $entry['name'], 'The name Jane is not found in the entry.');
+ // The surname NowDoe is found in the entry.
+ $this->assertEquals('NowDoe', $entry['surname'], 'The surname NowDoe is not found in the entry.');
+
+ // Delete the entry.
+ DbtngExampleStorage::delete($entry);
+
+ // Verify that now there are only 3 records.
+ $result = DbtngExampleStorage::load();
+ // Found only three records, a record was deleted.
+ $this->assertCount(3, $result, 'Did not find only three records, a record might not have been deleted.');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/email_example/email_example.info.yml b/sites/all/modules/examples/examples/email_example/email_example.info.yml
new file mode 100644
index 000000000..2cacfd577
--- /dev/null
+++ b/sites/all/modules/examples/examples/email_example/email_example.info.yml
@@ -0,0 +1,14 @@
+name: Email Example
+type: module
+description: Demonstrates how to send and alter Drupal-generated email messages.
+package: Example modules
+# core: 8.x
+dependencies:
+ - examples:examples
+ - drupal:node
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/email_example/email_example.links.menu.yml b/sites/all/modules/examples/examples/email_example/email_example.links.menu.yml
new file mode 100644
index 000000000..50f95e021
--- /dev/null
+++ b/sites/all/modules/examples/examples/email_example/email_example.links.menu.yml
@@ -0,0 +1,4 @@
+email_example.description:
+ title: 'E-mail Example: Contact Form'
+ description: 'Callback for generating form.'
+ route_name: email_example.description
diff --git a/sites/all/modules/examples/examples/email_example/email_example.module b/sites/all/modules/examples/examples/email_example/email_example.module
new file mode 100644
index 000000000..94beb6cb9
--- /dev/null
+++ b/sites/all/modules/examples/examples/email_example/email_example.module
@@ -0,0 +1,99 @@
+ $message['langcode'],
+ ];
+
+ switch ($key) {
+ // Send a simple message from the contact form.
+ case 'contact_message':
+ $from = \Drupal::config('system.site')->get('mail');
+ $message['subject'] = t('E-mail sent from @site-name', ['@site-name' => $from], $options);
+ // Note that the message body is an array, not a string.
+ $account = \Drupal::currentUser();
+ $message['body'][] = t('@name sent you the following message:', ['@name' => $account->getUsername()], $options);
+ // Because this is just user-entered text, we do not need to translate it.
+ // Since user-entered text may have unintentional HTML entities in it like
+ // '<' or '>', we need to make sure these entities are properly escaped,
+ // as the body will later be transformed from HTML to text, meaning
+ // that a normal use of '<' will result in truncation of the message.
+ $message['body'][] = SafeMarkup::checkPlain($params['message']);
+ break;
+ }
+}
+
+/**
+ * Implements hook_mail_alter().
+ *
+ * This function is not required to send an email using Drupal's mail system.
+ *
+ * hook_mail_alter() provides an interface to alter any aspect of email sent by
+ * Drupal. You can use this hook to add a common site footer to all outgoing
+ * email, add extra header fields, and/or modify the email in anyway. HTML-izing
+ * the outgoing email is one possibility.
+ */
+function email_example_mail_alter(&$message) {
+ // For the purpose of this example, modify all the outgoing messages and
+ // attach a site signature. The signature will be translated to the language
+ // in which message was built.
+ $options = [
+ 'langcode' => $message['langcode'],
+ ];
+
+ $signature = t("\n--\nMail altered by email_example module.", [], $options);
+ if (is_array($message['body'])) {
+ $message['body'][] = $signature;
+ }
+ else {
+ // Some modules use the body as a string, erroneously.
+ $message['body'] .= $signature;
+ }
+}
+
+/**
+ * @} End of "defgroup email_example".
+ */
diff --git a/sites/all/modules/examples/examples/email_example/email_example.routing.yml b/sites/all/modules/examples/examples/email_example/email_example.routing.yml
new file mode 100644
index 000000000..cefa64432
--- /dev/null
+++ b/sites/all/modules/examples/examples/email_example/email_example.routing.yml
@@ -0,0 +1,6 @@
+email_example.description:
+ path: 'examples/email-example'
+ defaults:
+ _form: '\Drupal\email_example\Form\EmailExampleGetFormPage'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/email_example/src/Form/EmailExampleGetFormPage.php b/sites/all/modules/examples/examples/email_example/src/Form/EmailExampleGetFormPage.php
new file mode 100644
index 000000000..8951e0560
--- /dev/null
+++ b/sites/all/modules/examples/examples/email_example/src/Form/EmailExampleGetFormPage.php
@@ -0,0 +1,148 @@
+mailManager = $mail_manager;
+ $this->languageManager = $language_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.mail'),
+ $container->get('language_manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'email_example';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['intro'] = [
+ '#markup' => t('Use this form to send a message to an e-mail address. No spamming!'),
+ ];
+ $form['email'] = [
+ '#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#required' => TRUE,
+ ];
+ $form['message'] = [
+ '#type' => 'textarea',
+ '#title' => t('Message'),
+ '#required' => TRUE,
+ ];
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ if (!valid_email_address($form_state->getValue('email'))) {
+ $form_state->setErrorByName('email', t('That e-mail address is not valid.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+
+ // All system mails need to specify the module and template key (mirrored
+ // from hook_mail()) that the message they want to send comes from.
+ $module = 'email_example';
+ $key = 'contact_message';
+
+ // Specify 'to' and 'from' addresses.
+ $to = $form_values['email'];
+ $from = $this->config('system.site')->get('mail');
+
+ // "params" loads in additional context for email content completion in
+ // hook_mail(). In this case, we want to pass in the values the user entered
+ // into the form, which include the message body in $form_values['message'].
+ $params = $form_values;
+
+ // The language of the e-mail. This will one of three values:
+ // - $account->getPreferredLangcode(): Used for sending mail to a particular
+ // website user, so that the mail appears in their preferred language.
+ // - \Drupal::currentUser()->getPreferredLangcode(): Used when sending a
+ // mail back to the user currently viewing the site. This will send it in
+ // the language they're currently using.
+ // - \Drupal::languageManager()->getDefaultLanguage()->getId: Used when
+ // sending mail to a pre-existing, 'neutral' address, such as the system
+ // e-mail address, or when you're unsure of the language preferences of
+ // the intended recipient.
+ //
+ // Since in our case, we are sending a message to a random e-mail address
+ // that is not necessarily tied to a user account, we will use the site's
+ // default language.
+ $language_code = $this->languageManager->getDefaultLanguage()->getId();
+
+ // Whether or not to automatically send the mail when we call mail() on the
+ // mail manager. This defaults to TRUE, and is normally what you want unless
+ // you need to do additional processing before the mail manager sends the
+ // message.
+ $send_now = TRUE;
+ // Send the mail, and check for success. Note that this does not guarantee
+ // message delivery; only that there were no PHP-related issues encountered
+ // while sending.
+ $result = $this->mailManager->mail($module, $key, $to, $language_code, $params, $from, $send_now);
+ if ($result['result'] == TRUE) {
+ drupal_set_message(t('Your message has been sent.'));
+ }
+ else {
+ drupal_set_message(t('There was a problem sending your message and it was not sent.'), 'error');
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/email_example/tests/src/Functional/EmailExampleTest.php b/sites/all/modules/examples/examples/email_example/tests/src/Functional/EmailExampleTest.php
new file mode 100644
index 000000000..2c7ab55d0
--- /dev/null
+++ b/sites/all/modules/examples/examples/email_example/tests/src/Functional/EmailExampleTest.php
@@ -0,0 +1,73 @@
+assertSession();
+ // Test for a link to the email_example in the Tools menu.
+ $this->drupalGet('');
+ $assert->statusCodeEquals(200);
+ $assert->linkByHrefExists('examples/email-example');
+
+ // Verify if we can successfully access the email_example page.
+ $this->drupalGet('examples/email-example');
+ $assert->statusCodeEquals(200);
+
+ // Verifiy email form has email & message fields.
+ $assert->fieldValueEquals('edit-email', NULL);
+ $assert->fieldValueEquals('edit-message', NULL);
+
+ // Verifiy email form is submitted.
+ $edit = ['email' => 'example@example.com', 'message' => 'test'];
+ $this->drupalPostForm('examples/email-example', $edit, 'Submit');
+ $assert->statusCodeEquals(200);
+
+ // Verifiy comfirmation page.
+ $assert->pageTextContains('Your message has been sent.');
+ $this->assertMailString('to', $edit['email'], 1);
+
+ // Verifiy correct email recieved.
+ $from = \Drupal::config('system.site')->get('mail');
+ $this->assertMailString('subject', "E-mail sent from $from", 1);
+ $this->assertMailString('body', $edit['message'], 1);
+ $this->assertMailString('body', "\n--\nMail altered by email_example module.", 1);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/events_example/events_example.info.yml b/sites/all/modules/examples/examples/events_example/events_example.info.yml
new file mode 100644
index 000000000..940cb520f
--- /dev/null
+++ b/sites/all/modules/examples/examples/events_example/events_example.info.yml
@@ -0,0 +1,13 @@
+name: Events Example
+type: module
+description: Provides an example of subscribing to and dispatching events.
+package: Example modules
+# core: 8.x
+dependencies:
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/events_example/events_example.links.menu.yml b/sites/all/modules/examples/examples/events_example/events_example.links.menu.yml
new file mode 100644
index 000000000..61eb582a0
--- /dev/null
+++ b/sites/all/modules/examples/examples/events_example/events_example.links.menu.yml
@@ -0,0 +1,4 @@
+events_example.description:
+ title: 'Events Example'
+ description: 'Example of dispatching and subscribing to events.'
+ route_name: events_example.description
diff --git a/sites/all/modules/examples/examples/events_example/events_example.module b/sites/all/modules/examples/examples/events_example/events_example.module
new file mode 100644
index 000000000..0bfac70fe
--- /dev/null
+++ b/sites/all/modules/examples/examples/events_example/events_example.module
@@ -0,0 +1,75 @@
+type = $type;
+ $this->report = $report;
+ }
+
+ /**
+ * Get the incident type.
+ *
+ * @return string
+ * The type of report.
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * Get the detailed incident report.
+ *
+ * @return string
+ * The text of the report.
+ */
+ public function getReport() {
+ return $this->report;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/events_example/src/EventSubscriber/EventsExampleSubscriber.php b/sites/all/modules/examples/examples/events_example/src/EventSubscriber/EventsExampleSubscriber.php
new file mode 100644
index 000000000..c78cfd925
--- /dev/null
+++ b/sites/all/modules/examples/examples/events_example/src/EventSubscriber/EventsExampleSubscriber.php
@@ -0,0 +1,111 @@
+getType() == 'stolen_princess') {
+ drupal_set_message($this->t('Mario has been alerted. Thank you. This message was set by an event subscriber. See \Drupal\events_example\EventSubscriber\EventsExampleSubscriber::notifyMario()'), 'status');
+ }
+ }
+
+ /**
+ * Let Batman know about any events involving the Joker.
+ *
+ * @param \Drupal\events_example\Event\IncidentReportEvent $event
+ * The event object containing the incident report.
+ */
+ public function notifyBatman(IncidentReportEvent $event) {
+ if ($event->getType() == 'joker') {
+ drupal_set_message($this->t('Batman has been alerted. Thank you. This message was set by an event subscriber. See \Drupal\events_example\EventSubscriber\EventsExampleSubscriber::notifyBatman()'), 'status');
+ // Optionally use the event object to stop propagation.
+ // If there are other subscribers that have not been called yet this will
+ // cause them to be skipped.
+ $event->stopPropagation();
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/events_example/src/Form/EventsExampleForm.php b/sites/all/modules/examples/examples/events_example/src/Form/EventsExampleForm.php
new file mode 100644
index 000000000..edc0f460c
--- /dev/null
+++ b/sites/all/modules/examples/examples/events_example/src/Form/EventsExampleForm.php
@@ -0,0 +1,130 @@
+eventDispatcher = $event_dispatcher;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('event_dispatcher')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['intro'] = [
+ '#markup' => '
' . $this->t('This form demonstrates subscribing to, and dispatching, events. When the form is submitted an event is dispatched indicating a new report has been submitted. Event subscribers respond to this event with various messages depending on the incident type. Review the code for the events_example module to see how it works.') . '
',
+ ];
+
+ $form['incident_type'] = [
+ '#type' => 'radios',
+ '#required' => TRUE,
+ '#title' => t('What type of incident do you want to report?'),
+ '#options' => [
+ 'stolen_princess' => $this->t('Missing princess'),
+ 'cat' => $this->t('Cat stuck in tree'),
+ 'joker' => $this->t('Something involving the Joker'),
+ ],
+ ];
+
+ $form['incident'] = [
+ '#type' => 'textarea',
+ '#required' => FALSE,
+ '#title' => t('Incident report'),
+ '#description' => t('Describe the incident in detail. This information will be passed along to all crime fighters.'),
+ '#cols' => 60,
+ '#rows' => 5,
+ ];
+
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'events_example_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $type = $form_state->getValue('incident_type');
+ $report = $form_state->getValue('incident');
+
+ // When dispatching, or triggering, an event start by constructing a new
+ // event object. Then use the event dispatcher service to notify any event
+ // subscribers. Event objects are used to transport relevant data to any
+ // subscribers, as well as keep track of the current state of an event. It
+ // is best practice to create a unique class wrapping
+ // \Symfony\Component\EventDispatcher\Event.
+ $event = new IncidentReportEvent($type, $report);
+
+ // Dispatch an event by specifying which event, and providing an event
+ // object. Rather than hard code the event name you should use a constant
+ // to represent the event being dispatched. The constant serves as a
+ // location for documentation of the event, and ensures your code is future
+ // proofed against event name changes.
+ $this->eventDispatcher->dispatch(IncidentEvents::NEW_REPORT, $event);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/events_example/tests/src/Functional/EventsExampleTest.php b/sites/all/modules/examples/examples/events_example/tests/src/Functional/EventsExampleTest.php
new file mode 100644
index 000000000..57bc9f5f3
--- /dev/null
+++ b/sites/all/modules/examples/examples/events_example/tests/src/Functional/EventsExampleTest.php
@@ -0,0 +1,64 @@
+drupalGet('examples/events-example');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Verify the page contains the required form fields.
+ $this->assertSession()->fieldExists('incident_type');
+ $this->assertSession()->fieldExists('incident');
+
+ // Submit the form with an incident type of 'stolen_princess'. This does a
+ // couple of things. Fist of all, it ensures that our code in
+ // EventsExampleForm::submitForm() that dispatches events works. If it did
+ // not work, no event would be dispatched, and the message below would never
+ // get displayed. Secondly, it tests that our
+ // EventsExampleSubscriber::notifyMario() event subscriber is triggered for
+ // incidents of the type 'stolen_princess'.
+ $values = [
+ 'incident_type' => 'stolen_princess',
+ 'incident' => $this->randomString(),
+ ];
+ $this->drupalPostForm('examples/events-example', $values, 'Submit');
+ $this->assertSession()->pageTextContains('Mario has been alerted. Thank you.');
+
+ // Fill out the form again, this time testing that the
+ // EventsExampleSubscriber::notifyBatman() subscriber is working.
+ $values = [
+ 'incident_type' => 'joker',
+ 'incident' => $this->randomString(),
+ ];
+ $this->drupalPostForm('examples/events-example', $values, 'Submit');
+ $this->assertSession()->pageTextContains('Batman has been alerted. Thank you.');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/events_example/tests/src/Kernel/EventsExampleServiceTest.php b/sites/all/modules/examples/examples/events_example/tests/src/Kernel/EventsExampleServiceTest.php
new file mode 100644
index 000000000..409f6d927
--- /dev/null
+++ b/sites/all/modules/examples/examples/events_example/tests/src/Kernel/EventsExampleServiceTest.php
@@ -0,0 +1,29 @@
+container->get('events_example_subscriber');
+ $this->assertInstanceOf(EventsExampleSubscriber::class, $subscriber);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/examples.info.yml b/sites/all/modules/examples/examples/examples.info.yml
new file mode 100644
index 000000000..6ca2e2872
--- /dev/null
+++ b/sites/all/modules/examples/examples/examples.info.yml
@@ -0,0 +1,13 @@
+name: Examples for Developers
+type: module
+description: 'Provides example code for you to learn from.'
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:toolbar
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/examples.libraries.yml b/sites/all/modules/examples/examples/examples.libraries.yml
new file mode 100644
index 000000000..e63351a57
--- /dev/null
+++ b/sites/all/modules/examples/examples/examples.libraries.yml
@@ -0,0 +1,5 @@
+examples.icons:
+ version: VERSION
+ css:
+ theme:
+ css/examples.icons.css: {}
diff --git a/sites/all/modules/examples/examples/examples.module b/sites/all/modules/examples/examples/examples.module
new file mode 100644
index 000000000..96cb44ffb
--- /dev/null
+++ b/sites/all/modules/examples/examples/examples.module
@@ -0,0 +1,131 @@
+ 'ajax_example.description',
+ 'batch_example' => 'batch_example.form',
+ 'block_example' => 'block_example.description',
+ 'cache_example' => 'cache_example.description',
+ 'config_entity_example' => 'entity.robot.list',
+ 'content_entity_example' => 'entity.content_entity_example_contact.collection',
+ 'cron_example' => 'cron_example',
+ 'dbtng_example' => 'dbtng_example',
+ 'email_example' => 'email_example.description',
+ 'events_example' => 'events_example.description',
+ 'form_api_example' => 'form_api_example.description',
+ 'field_example' => 'field_example.description',
+ 'field_permission_example' => 'field_permission_example.description',
+ 'file_example' => 'file_example.fileapi',
+ 'hooks_example' => 'hooks_example.description',
+ 'js_example' => 'js_example.info',
+ 'node_type_example' => 'config_node_type_example.description',
+ 'page_example' => 'page_example_description',
+ 'pager_example' => 'pager_example.page',
+ 'phpunit_example' => 'phpunit_example_description',
+ 'plugin_type_example' => 'plugin_type_example.description',
+ 'simpletest_example' => 'simpletest_example_description',
+ 'tabledrag_example' => 'tabledrag_example.description',
+ 'stream_wrapper_example' => 'stream_wrapper_example.description',
+ 'testing_example' => 'testing_example.description',
+ 'queue_example' => 'queue_example',
+ 'tablesort_example' => 'tablesort_example_description',
+ 'tour_example' => 'tour_example_description',
+ ];
+
+ // Build a list of links for the menu.
+ $links = [];
+ foreach ($examples as $module => $route) {
+ // Get the module info (title, description) from Drupal.
+ $info = system_get_info('module', $module);
+
+ // If there's no info, the example isn't enabled, so don't display it.
+ if (!empty($info)) {
+ $links[$module] = [
+ 'title' => t($info['name']),
+ 'url' => Url::fromRoute($route),
+ 'attributes' => [
+ 'title' => t($info['description']),
+ ],
+ ];
+ }
+ }
+
+ // Add a link to enable all examples.
+ $links['enable_examples'] = [
+ 'title' => t('Enable Examples'),
+ 'url' => Url::fromRoute('system.modules_list'),
+ 'options' => [
+ 'title' => t('Enable more examples in on the Extend page.'),
+ ],
+ 'fragment' => 'edit-modules-example-modules',
+ ];
+
+ // Create the examples toolbar render array.
+ $items['examples'] = [
+ '#type' => 'toolbar_item',
+ 'tab' => [
+ '#type' => 'link',
+ '#title' => t('Examples'),
+ '#url' => Url::fromRoute(''),
+ '#attributes' => [
+ 'title' => t('Developer Examples'),
+ 'class' => ['toolbar-icon', 'toolbar-icon-examples'],
+ ],
+ ],
+ 'tray' => [
+ '#heading' => t('Developer Examples'),
+ 'shortcuts' => [
+ '#theme' => 'links__toolbar_example',
+ '#links' => $links,
+ '#attributes' => [
+ 'class' => ['toolbar-menu'],
+ ],
+ ],
+ ],
+ '#weight' => 99,
+ '#attached' => [
+ 'library' => [
+ 'examples/examples.icons',
+ ],
+ ],
+ ];
+
+ return $items;
+}
+
+/**
+ * @} End of 'defgroup examples'.
+ */
diff --git a/sites/all/modules/examples/examples/field_example/config/schema/field_example.schema.yml b/sites/all/modules/examples/examples/field_example/config/schema/field_example.schema.yml
new file mode 100644
index 000000000..c2e40ea05
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/config/schema/field_example.schema.yml
@@ -0,0 +1,12 @@
+# @see Configuration schema/metadata at https://drupal.org/node/1905070
+#
+field.field_example_rgb.value:
+ type: sequence
+ label: 'Default value'
+ sequence:
+ - type: mapping
+ label: 'Default'
+ mapping:
+ value:
+ type: string
+ label: 'Value'
diff --git a/sites/all/modules/examples/examples/field_example/field_example.info.yml b/sites/all/modules/examples/examples/field_example/field_example.info.yml
new file mode 100644
index 000000000..954a19ff2
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/field_example.info.yml
@@ -0,0 +1,15 @@
+name: Field Example
+type: module
+description: Demonstrates the Field API via some field plugin implementations.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:color
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/field_example/field_example.js b/sites/all/modules/examples/examples/field_example/field_example.js
new file mode 100644
index 000000000..1edb9ec55
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/field_example.js
@@ -0,0 +1,27 @@
+/**
+ * @file
+ * Javascript for Field Example.
+ */
+
+/**
+ * Provides a farbtastic colorpicker for the fancier widget.
+ */
+(function ($) {
+
+ 'use strict';
+
+ Drupal.behaviors.field_example_colorpicker = {
+ attach: function () {
+ $('.edit-field-example-colorpicker').on('focus', function (event) {
+ var edit_field = this;
+ var picker = $(this).closest('div').parent().find('.field-example-colorpicker');
+ // Hide all color pickers except this one.
+ $('.field-example-colorpicker').hide();
+ $(picker).show();
+ $.farbtastic(picker, function (color) {
+ edit_field.value = color;
+ }).setColor(edit_field.value);
+ });
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/examples/examples/field_example/field_example.libraries.yml b/sites/all/modules/examples/examples/field_example/field_example.libraries.yml
new file mode 100644
index 000000000..7331ff916
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/field_example.libraries.yml
@@ -0,0 +1,4 @@
+colorpicker:
+ version: 1.x
+ js:
+ field_example.js: {}
diff --git a/sites/all/modules/examples/examples/field_example/field_example.links.menu.yml b/sites/all/modules/examples/examples/field_example/field_example.links.menu.yml
new file mode 100644
index 000000000..07b81b3be
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/field_example.links.menu.yml
@@ -0,0 +1,3 @@
+field_example.description:
+ title: Field Example
+ route_name: field_example.description
diff --git a/sites/all/modules/examples/examples/field_example/field_example.module b/sites/all/modules/examples/examples/field_example/field_example.module
new file mode 100644
index 000000000..7f005ab73
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/field_example.module
@@ -0,0 +1,34 @@
+administratorAccount = $this->drupalCreateUser($permissions);
+ parent::drupalLogin($this->administratorAccount);
+
+ // Prepare a new content type where the field will be added.
+ $this->contentTypeName = strtolower($this->randomMachineName(10));
+ $this->drupalGet('admin/structure/types/add');
+ $edit = [
+ 'name' => $this->contentTypeName,
+ 'type' => $this->contentTypeName,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save and manage fields'));
+ $this->assertText(t('The content type @name has been added.', ['@name' => $this->contentTypeName]));
+
+ // Reset the permission cache.
+ $create_permission = 'create ' . $this->contentTypeName . ' content';
+ $this->checkPermissions([$create_permission], TRUE);
+
+ // Now that we have a new content type, create a user that has privileges
+ // on the content type.
+ $this->authorAccount = $this->drupalCreateUser([$create_permission]);
+ }
+
+ /**
+ * Create a field on the content type created during setUp().
+ *
+ * @param string $type
+ * The storage field type to create.
+ * @param string $widget_type
+ * The widget to use when editing this field.
+ * @param int|string $cardinality
+ * Cardinality of the field. Use -1 to signify 'unlimited'.
+ * @param string $fieldFormatter
+ * The formatter to use when editing this field.
+ *
+ * @return string
+ * Name of the field, like field_something
+ */
+ protected function createField($type = 'field_example_rgb', $widget_type = 'field_example_text', $cardinality = '1', $fieldFormatter = 'field_example_simple_text') {
+ $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/fields');
+
+ // Go to the 'Add field' page.
+ $this->clickLink('Add field');
+
+ // Make a name for this field.
+ $field_name = strtolower($this->randomMachineName(10));
+
+ // Fill out the field form.
+ $edit = [
+ 'new_storage_type' => $type,
+ 'field_name' => $field_name,
+ 'label' => $field_name,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save and continue'));
+
+ // Fill out the $cardinality form as if we're not using an unlimited number
+ // of values.
+ $edit = [
+ 'cardinality' => 'number',
+ 'cardinality_number' => (string) $cardinality,
+ ];
+ // If we have -1 for $cardinality, we should change the form's drop-down
+ // from 'Number' to 'Unlimited'.
+ if (-1 == $cardinality) {
+ $edit = [
+ 'cardinality' => '-1',
+ 'cardinality_number' => '1',
+ ];
+ }
+
+ // And now we save the cardinality settings.
+ $this->drupalPostForm(NULL, $edit, t('Save field settings'));
+ debug(
+ t('Saved settings for field %field_name with widget %widget_type and cardinality %cardinality',
+ [
+ '%field_name' => $field_name,
+ '%widget_type' => $widget_type,
+ '%cardinality' => $cardinality,
+ ]
+ )
+ );
+ $this->assertText(t('Updated field @name field settings.', ['@name' => $field_name]));
+
+ // Set the widget type for the newly created field.
+ $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/form-display');
+ $edit = [
+ 'fields[field_' . $field_name . '][type]' => $widget_type,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+
+ // Set the field formatter for the newly created field.
+ $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/display');
+ $edit1 = [
+ 'fields[field_' . $field_name . '][type]' => $fieldFormatter,
+ ];
+ $this->drupalPostForm(NULL, $edit1, t('Save'));
+
+ return $field_name;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroudFormatter.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroudFormatter.php
new file mode 100644
index 000000000..581bce3a3
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroudFormatter.php
@@ -0,0 +1,40 @@
+ $item) {
+ $elements[$delta] = [
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => t('The content area color has been changed to @code', ['@code' => $item->value]),
+ '#attributes' => [
+ 'style' => 'background-color: ' . $item->value,
+ ],
+ ];
+ }
+ return $elements;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php
new file mode 100644
index 000000000..6a382a470
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php
@@ -0,0 +1,45 @@
+ $item) {
+ $elements[$delta] = [
+ // We create a render array to produce the desired markup,
+ // "
The color code ... #hexcolor
".
+ // See theme_html_tag().
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#attributes' => [
+ 'style' => 'color: ' . $item->value,
+ ],
+ '#value' => $this->t('The color code in this field is @code', ['@code' => $item->value]),
+ ];
+ }
+
+ return $elements;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldType/RgbItem.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldType/RgbItem.php
new file mode 100644
index 000000000..55c0617e1
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldType/RgbItem.php
@@ -0,0 +1,55 @@
+ [
+ 'value' => [
+ 'type' => 'text',
+ 'size' => 'tiny',
+ 'not null' => FALSE,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEmpty() {
+ $value = $this->get('value')->getValue();
+ return $value === NULL || $value === '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+ $properties['value'] = DataDefinition::create('string')
+ ->setLabel(t('Hex value'));
+
+ return $properties;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/ColorPickerWidget.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/ColorPickerWidget.php
new file mode 100644
index 000000000..ebe07b8bb
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/ColorPickerWidget.php
@@ -0,0 +1,43 @@
+ '',
+ '#attributes' => ['class' => ['edit-field-example-colorpicker']],
+ '#attached' => [
+ // Add Farbtastic color picker and javascript file to trigger the
+ // colorpicker.
+ 'library' => [
+ 'core/jquery.farbtastic',
+ 'field_example/colorpicker',
+ ],
+ ],
+ ];
+
+ return $element;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/Text3Widget.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/Text3Widget.php
new file mode 100644
index 000000000..f8690a4c0
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/Text3Widget.php
@@ -0,0 +1,87 @@
+value) ? $items[$delta]->value : '';
+ // Parse the single hex string into RBG values.
+ if (!empty($value)) {
+ preg_match_all('@..@', substr($value, 1), $match);
+ }
+ else {
+ $match = [[]];
+ }
+
+ // Set up the form element for this widget.
+ $element += [
+ '#type' => 'details',
+ '#element_validate' => [
+ [$this, 'validate'],
+ ],
+ ];
+
+ // Add in the RGB textfield elements.
+ foreach (['r' => t('Red'), 'g' => t('Green'), 'b' => t('Blue')] as $key => $title) {
+ $element[$key] = [
+ '#type' => 'textfield',
+ '#title' => $title,
+ '#size' => 2,
+ '#default_value' => array_shift($match[0]),
+ '#attributes' => ['class' => ['rgb-entry']],
+ '#description' => t('The 2-digit hexadecimal representation of @color saturation, like "a1" or "ff"', ['@color' => $title]),
+ ];
+ // Since Form API doesn't allow a fieldset to be required, we
+ // have to require each field element individually.
+ if ($element['#required']) {
+ $element[$key]['#required'] = TRUE;
+ }
+ }
+ return ['value' => $element];
+ }
+
+ /**
+ * Validate the fields and convert them into a single value as text.
+ */
+ public function validate($element, FormStateInterface $form_state) {
+ // Validate each of the textfield entries.
+ $values = [];
+ foreach (['r', 'g', 'b'] as $colorfield) {
+ $values[$colorfield] = $element[$colorfield]['#value'];
+ // If they left any empty, we'll set the value empty and quit.
+ if (strlen($values[$colorfield]) == 0) {
+ $form_state->setValueForElement($element, '');
+ return;
+ }
+ // If they gave us anything that's not hex, reject it.
+ if ((strlen($values[$colorfield]) != 2) || !ctype_xdigit($values[$colorfield])) {
+ $form_state->setError($element[$colorfield], $form_state, t("Saturation value must be a 2-digit hexadecimal value between 00 and ff."));
+ }
+ }
+
+ // Set the value of the entire form element.
+ $value = strtolower(sprintf('#%02s%02s%02s', $values['r'], $values['g'], $values['b']));
+ $form_state->setValueForElement($element, $value);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/TextWidget.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/TextWidget.php
new file mode 100644
index 000000000..fcebc2d4f
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/TextWidget.php
@@ -0,0 +1,54 @@
+value) ? $items[$delta]->value : '';
+ $element += [
+ '#type' => 'textfield',
+ '#default_value' => $value,
+ '#size' => 7,
+ '#maxlength' => 7,
+ '#element_validate' => [
+ [$this, 'validate'],
+ ],
+ ];
+ return ['value' => $element];
+ }
+
+ /**
+ * Validate the color text field.
+ */
+ public function validate($element, FormStateInterface $form_state) {
+ $value = $element['#value'];
+ if (strlen($value) == 0) {
+ $form_state->setValueForElement($element, '');
+ return;
+ }
+ if (!preg_match('/^#([a-f0-9]{6})$/iD', strtolower($value))) {
+ $form_state->setError($element, t("Color must be a 6-digit hexadecimal value, suitable for CSS."));
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/templates/description.html.twig b/sites/all/modules/examples/examples/field_example/templates/description.html.twig
new file mode 100644
index 000000000..50199bf16
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/templates/description.html.twig
@@ -0,0 +1,12 @@
+{#
+
+Description text for the Field Example.
+
+#}
+
+{% trans %}
+
+
The Field Example provides a field composed of an HTML RGB value, like
+ #ff00ff. To use it, add the field to a content type.
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorBackgroundFormatterTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorBackgroundFormatterTest.php
new file mode 100644
index 000000000..4cf56246d
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorBackgroundFormatterTest.php
@@ -0,0 +1,98 @@
+assertSession();
+ // Login with Admin and create a field.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '1', 'field_example_color_background');
+
+ // Login with Author user for content creation.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Details to be submitted for content creation.
+ $title = $this->randomMachineName(20);
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value]' => '#00ff00',
+ ];
+
+ // Submit the content creation form.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify color.
+ $assert->pageTextContains('The content area color has been changed to #00ff00');
+ }
+
+ /**
+ * Tests a multi-value field.
+ *
+ * Test the following scenarios in Field example:
+ * - Creates a content type.
+ * - Adds a multivalued field_example_rgb to it.
+ * - Creates a node of the new type.
+ * - Populates the multivalued field with two items.
+ * - Tests the result.
+ */
+ public function testMultiValueField() {
+ $assert = $this->assertSession();
+
+ // Login with Admin and create a field.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '-1', 'field_example_color_background');
+
+ // Login with Author user for content creation.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Details to be submitted for content creation.
+ $title = $this->randomMachineName(20);
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value]' => '#00ff00',
+ ];
+
+ // Add another field value.
+ $this->drupalPostForm(NULL, $edit, 'Add another item');
+
+ // Set value for newly added item.
+ $edit = [
+ 'field_' . $this->fieldName . '[1][value]' => '#ffff4f',
+ ];
+
+ // Submit the content creation form.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify color.
+ $assert->pageTextContains('The content area color has been changed to #00ff00');
+ $assert->pageTextContains('The content area color has been changed to #ffff4f');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorPickerWidgetTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorPickerWidgetTest.php
new file mode 100644
index 000000000..26626cc93
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorPickerWidgetTest.php
@@ -0,0 +1,99 @@
+assertSession();
+ // Login with Admin and create a field.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '1');
+
+ // Login with Author user for content creation.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Details to be submitted for content creation.
+ $title = $this->randomMachineName(20);
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value]' => '#00ff00',
+ ];
+
+ // Submit the content creation form.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify color.
+ $assert->pageTextContains('The color code in this field is #00ff00');
+ }
+
+ /**
+ * Field example scenario tests.
+ *
+ * The following scenarios:
+ * - Creates a content type.
+ * - Adds a multivalued field_example_rgb to it.
+ * - Creates a node of the new type.
+ * - Populates the multivalued field with two items.
+ * - Tests the result.
+ */
+ public function testMultiValueField() {
+ $assert = $this->assertSession();
+
+ // Login with Admin and create a field.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '-1');
+
+ // Login with Author user for content creation.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Details to be submitted for content creation.
+ $title = $this->randomMachineName(20);
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value]' => '#00ff00',
+ ];
+
+ // Add another field value.
+ $this->drupalPostForm(NULL, $edit, 'Add another item');
+
+ // Set value for newly added item.
+ $edit = [
+ 'field_' . $this->fieldName . '[1][value]' => '#ffffff',
+ ];
+
+ // Submit the content creation form.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify color.
+ $assert->pageTextContains('The color code in this field is #00ff00');
+ $assert->pageTextContains('The color code in this field is #ffffff');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleBrowserTestBase.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleBrowserTestBase.php
new file mode 100644
index 000000000..0a8b1236d
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleBrowserTestBase.php
@@ -0,0 +1,167 @@
+administratorAccount = $this->drupalCreateUser($permissions);
+ parent::drupalLogin($this->administratorAccount);
+
+ // Prepare a new content type where the field will be added.
+ $this->contentTypeName = strtolower($this->randomMachineName(10));
+ $this->drupalGet('admin/structure/types/add');
+ $edit = [
+ 'name' => $this->contentTypeName,
+ 'type' => $this->contentTypeName,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save and manage fields'));
+ $this->assertText(t('The content type @name has been added.', ['@name' => $this->contentTypeName]));
+
+ // Reset the permission cache.
+ $create_permission = 'create ' . $this->contentTypeName . ' content';
+ $this->checkPermissions([$create_permission], TRUE);
+
+ // Now that we have a new content type, create a user that has privileges
+ // on the content type.
+ $this->authorAccount = $this->drupalCreateUser([$create_permission]);
+ }
+
+ /**
+ * Create a field on the content type created during setUp().
+ *
+ * @param string $type
+ * The storage field type to create.
+ * @param string $widget_type
+ * The widget to use when editing this field.
+ * @param int|string $cardinality
+ * Cardinality of the field. Use -1 to signify 'unlimited'.
+ * @param string $fieldFormatter
+ * The formatter to use when editing this field.
+ *
+ * @return string
+ * Name of the field, like field_something
+ */
+ protected function createField($type = 'field_example_rgb', $widget_type = 'field_example_text', $cardinality = '1', $fieldFormatter = 'field_example_simple_text') {
+ $assert = $this->assertSession();
+
+ $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/fields');
+
+ // Go to the 'Add field' page.
+ $this->clickLink('Add field');
+
+ // Make a name for this field.
+ $field_name = strtolower($this->randomMachineName(10));
+
+ // Fill out the field form.
+ $edit = [
+ 'new_storage_type' => $type,
+ 'field_name' => $field_name,
+ 'label' => $field_name,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save and continue'));
+
+ // Fill out the $cardinality form as if we're not using an unlimited number
+ // of values.
+ $edit = [
+ 'cardinality' => 'number',
+ 'cardinality_number' => (string) $cardinality,
+ ];
+ // If we have -1 for $cardinality, we should change the form's drop-down
+ // from 'Number' to 'Unlimited'.
+ if (-1 == $cardinality) {
+ $edit = [
+ 'cardinality' => '-1',
+ 'cardinality_number' => '1',
+ ];
+ }
+
+ // And now we save the cardinality settings.
+ $this->drupalPostForm(NULL, $edit, t('Save field settings'));
+ $this->verbose(
+ t('Saved settings for field %field_name with widget %widget_type and cardinality %cardinality',
+ [
+ '%field_name' => $field_name,
+ '%widget_type' => $widget_type,
+ '%cardinality' => $cardinality,
+ ]
+ )
+ );
+ $assert->pageTextContains(t('Updated field @name field settings.', ['@name' => $field_name]));
+
+ // Set the widget type for the newly created field.
+ $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/form-display');
+ $edit = [
+ 'fields[field_' . $field_name . '][type]' => $widget_type,
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+
+ // Set the field formatter for the newly created field.
+ $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/display');
+ $edit1 = [
+ 'fields[field_' . $field_name . '][type]' => $fieldFormatter,
+ ];
+ $this->drupalPostForm(NULL, $edit1, t('Save'));
+
+ return $field_name;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleMenuTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleMenuTest.php
new file mode 100644
index 000000000..bd53c3626
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleMenuTest.php
@@ -0,0 +1,50 @@
+assertSession();
+ $this->drupalGet('');
+ $assert->linkByHrefExists('examples/field-example');
+ }
+
+ /**
+ * Tests field_example menus.
+ */
+ public function testBlockExampleMenu() {
+ $assert = $this->assertSession();
+ $this->drupalGet('examples/field-example');
+ $assert->statusCodeEquals(200);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/Text3WidgetTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/Text3WidgetTest.php
new file mode 100644
index 000000000..93e3b1408
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/Text3WidgetTest.php
@@ -0,0 +1,104 @@
+assertSession();
+ // Add a single field as administrator user.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_3text', '1');
+ // Post-condition: Content type now has the desired field.
+ // Switch to the author user to create content with this type and field.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Fill the create form.
+ $title = 'test_title';
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value][r]' => '00',
+ 'field_' . $this->fieldName . '[0][value][g]' => '0a',
+ 'field_' . $this->fieldName . '[0][value][b]' => '01',
+ ];
+
+ // Create the content.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify the value is shown when viewing this node.
+ $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/p");
+ $this->assertEquals("The color code in this field is #000a01", (string) $field_p[0]->getText());
+ }
+
+ /**
+ * Test basic functionality of the example field.
+ *
+ * - Creates a content type.
+ * - Adds a single-valued field_example_rgb to it.
+ * - Adds a multivalued field_example_rgb to it.
+ * - Creates a node of the new type.
+ * - Populates the single-valued field.
+ * - Populates the multivalued field with two items.
+ * - Tests the result.
+ */
+ public function testMultiValueField() {
+ $assert = $this->assertSession();
+
+ // Add a single field as administrator user.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_3text', '-1');
+ // Post-condition: Content type now has the desired field.
+ // Switch to the author user to create content with this type and field.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Fill the create form.
+ $title = $this->randomMachineName(20);
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value][r]' => '00',
+ 'field_' . $this->fieldName . '[0][value][g]' => 'ff',
+ 'field_' . $this->fieldName . '[0][value][b]' => '00',
+ ];
+
+ // Add a 2nd item to the multivalue field, so hit "add another".
+ $this->drupalPostForm(NULL, $edit, 'Add another item');
+ $edit = [
+ 'field_' . $this->fieldName . '[1][value][r]' => 'ff',
+ 'field_' . $this->fieldName . '[1][value][g]' => 'ff',
+ 'field_' . $this->fieldName . '[1][value][b]' => 'ff',
+ ];
+
+ // Create the content.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify the values are shown when viewing this node.
+ $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/div/p");
+ $this->assertEquals('The color code in this field is #00ff00', (string) $field_p[0]->getText());
+ $this->assertEquals('The color code in this field is #ffffff', (string) $field_p[1]->getText());
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/TextWidgetTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/TextWidgetTest.php
new file mode 100644
index 000000000..cd4169465
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/TextWidgetTest.php
@@ -0,0 +1,104 @@
+assertSession();
+
+ // Add a single field as administrator user.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_text', '1');
+
+ // Now that we have a content type with the desired field, switch to the
+ // author user to create content with it.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Add a node.
+ $title = $this->randomMachineName(20);
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value]' => '#000001',
+ ];
+
+ // Create the content.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify the value is shown when viewing this node.
+ $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/p");
+ $this->assertEquals('The color code in this field is #000001', (string) $field_p[0]->getText());
+ }
+
+ /**
+ * Test basic functionality of the example field.
+ *
+ * - Creates a content type.
+ * - Adds a single-valued field_example_rgb to it.
+ * - Adds a multivalued field_example_rgb to it.
+ * - Creates a node of the new type.
+ * - Populates the single-valued field.
+ * - Populates the multivalued field with two items.
+ * - Tests the result.
+ */
+ public function testMultiValueField() {
+ $assert = $this->assertSession();
+
+ // Add a single field as administrator user.
+ $this->drupalLogin($this->administratorAccount);
+ $this->fieldName = $this->createField('field_example_rgb', 'field_example_text', '-1');
+
+ // Now that we have a content type with the desired field, switch to the
+ // author user to create content with it.
+ $this->drupalLogin($this->authorAccount);
+ $this->drupalGet('node/add/' . $this->contentTypeName);
+
+ // Add a node.
+ $title = $this->randomMachineName(20);
+ $edit = [
+ 'title[0][value]' => $title,
+ 'field_' . $this->fieldName . '[0][value]' => '#00ff00',
+ ];
+
+ // We want to add a 2nd item to the multivalue field, so hit "add another".
+ $this->drupalPostForm(NULL, $edit, 'Add another item');
+
+ $edit = [
+ 'field_' . $this->fieldName . '[1][value]' => '#ffffff',
+ ];
+
+ // Now we can fill in the second item in the multivalue field and save.
+ $this->drupalPostForm(NULL, $edit, 'Save');
+ $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title]));
+
+ // Verify the value is shown when viewing this node.
+ $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/div/p");
+ $this->assertEquals('The color code in this field is #00ff00', (string) $field_p[0]->getText());
+ $this->assertEquals('The color code in this field is #ffffff', (string) $field_p[1]->getText());
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_permission_example/css/field-permission-description.css b/sites/all/modules/examples/examples/field_permission_example/css/field-permission-description.css
new file mode 100644
index 000000000..a366cc58d
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/css/field-permission-description.css
@@ -0,0 +1,3 @@
+/**
+ * Field Permssions Example CSS
+ */
diff --git a/sites/all/modules/examples/examples/field_permission_example/css/field_permission_example.css b/sites/all/modules/examples/examples/field_permission_example/css/field_permission_example.css
new file mode 100644
index 000000000..5bd0b0572
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/css/field_permission_example.css
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * CSS for Field Example.
+ */
+.stickynote {
+ background: #fefabc;
+ padding: 0.8em;
+ font-family: cursive;
+ font-size: 1.1em;
+ color: #def9ff;
+ width: 15em;
+ -moz-transform: rotate(2deg);
+ -webkit-transform: rotate(2deg);
+ -o-transform: rotate(2deg);
+ -ms-transform: rotate(2deg);
+ transform: rotate(2deg);
+ -moz-box-shadow: 0 4px 6px #333;
+ -webkit-box-shadow: 0 4px 6px #333;
+ box-shadow: 0 4px 6px #333;
+}
diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.info.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.info.yml
new file mode 100644
index 000000000..cd906b500
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.info.yml
@@ -0,0 +1,14 @@
+name: Field Permission Example
+type: module
+description: Demonstrates how to create a field and implement access control over it.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.libraries.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.libraries.yml
new file mode 100644
index 000000000..9b4e619b1
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.libraries.yml
@@ -0,0 +1,10 @@
+fieldnote_sticky:
+ version: 1.x
+ css:
+ theme:
+ css/field_permission_example.css: {}
+field_permissions_description:
+ version: 1.x
+ css:
+ theme:
+ css/field-permission-description.css: {}
diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.links.menu.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.links.menu.yml
new file mode 100644
index 000000000..ce9a56b3d
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.links.menu.yml
@@ -0,0 +1,3 @@
+field_permission_example.description:
+ title: Field Permission Example
+ route_name: field_permission_example.description
diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.module b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.module
new file mode 100644
index 000000000..be38dfbf0
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.module
@@ -0,0 +1,196 @@
+hasPermission(). We also give special edit
+ * access to users with the 'bypass node access', 'administer content types'
+ * permissions, defined by the node module, and the 'administer the fieldnote
+ * field' we define for the module.
+ *
+ * One tricky part is that our field won't always be attached to nodes. It could
+ * be attached to any type of entity. Fortunately, most content entities
+ * implement EntityOwnerInterface, which gives us a way to check this. An
+ * exception to this is the User entity; here, we just check to see that the
+ * account name matches that of $account. We can get the entity itself by
+ * calling $items->getEntity(), since these "know" what entity they belong to.
+ *
+ * In a real application, we'd have use-case specific permissions which might be
+ * more complex than these. Or perhaps simpler.
+ *
+ * You can see a more complex field implementation in field_example.module.
+ *
+ * @see field_example
+ * @see field_example.module
+ * @see field_types
+ * @see field
+ */
+
+// Use statements to support hook_entity_field_access.
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Access\AccessResult;
+
+// Interfaces used by entities to declare "ownership".
+use Drupal\user\EntityOwnerInterface;
+use Drupal\user\UserInterface;
+
+// Use statements for hook_entity_test_access.
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Implements hook_entity_field_access().
+ *
+ * We want to make sure that fields aren't being seen or edited
+ * by those who shouldn't.
+ */
+function field_permission_example_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
+ // Find out what field we're looking at. If it isn't
+ // our sticky note widget, tell Drupal we don't care about its access.
+ if ($field_definition->getType() != 'field_permission_example_fieldnote') {
+ return AccessResult::neutral();
+ }
+
+ // First we'll check if the user has the 'superuser'
+ // permissions that node provides. This way administrators
+ // will be able to administer the content types.
+ if ($account->hasPermission('bypass node access')) {
+ drupal_set_message(t('User can bypass node access.'));
+ return AccessResult::allowed();
+ }
+ if ($account->hasPermission('administer content types', $account)) {
+ drupal_set_message(t('User can administer content types.'));
+ return AccessResult::allowed();
+ }
+ if ($account->hasPermission('administer the fieldnote field', $account)) {
+ drupal_set_message(t('User can administer this field.'));
+ return AccessResult::allowed();
+ }
+
+ // For anyone else, it depends on the desired operation.
+ if ($operation == 'view' and $account->hasPermission('view any fieldnote')) {
+ drupal_set_message(t('User can view any field note.'));
+ return AccessResult::allowed();
+ }
+
+ if ($operation == 'edit' and $account->hasPermission('edit any fieldnote')) {
+ drupal_set_message(t('User can edit any field note.'));
+ return AccessResult::allowed();
+ }
+
+ // At this point, we need to know if the user "owns" the entity we're attached
+ // to. If it's a user, we'll use the account name to test. Otherwise rely on
+ // the entity implementing the EntityOwnerInterface. Anything else can't be
+ // owned, and we'll refuse access.
+ if ($items) {
+ $entity = $items->getEntity();
+ if ((($entity instanceof EntityOwnerInterface) and
+ $entity->getOwner()->getAccountName() == $account->getAccountName()) or
+ (($entity instanceof UserInterface) and
+ $entity->name->value == $account->getAccountName())
+ ) {
+ if ($operation == 'view' and $account->hasPermission('view own fieldnote')) {
+ drupal_set_message(t('User can view their own field note.'));
+ return AccessResult::allowed();
+ }
+ if ($operation == 'edit' and $account->hasPermission('edit own fieldnote')) {
+ drupal_set_message(t('User can edit their own field note.'));
+ return AccessResult::allowed();
+ }
+ }
+ }
+ // Anything else on this field is forbidden.
+ return AccessResult::forbidden();
+}
+
+
+/**
+ * Implements hook_ENTITY_TYPE_access().
+ *
+ * Note: this routine is added so we can more easily test our access code. Core
+ * defines an entity_test entity that is used for testing fields in core. We add
+ * this routine to make the entity_test entity editable by our tests.
+ */
+function field_permission_example_entity_test_access(EntityInterface $entity, $operation, AccountInterface $account, $langcode) {
+ if ($operation == 'edit') {
+ $perms = [
+ 'administer the fieldnote field',
+ 'edit any fieldnote',
+ 'edit own fieldnote',
+ ];
+ foreach ($perms as $perm) {
+ if ($account->hasPermission($perm)) {
+ return AccessResult::allowed();
+ }
+ }
+ }
+ return AccessResult::neutral();
+}
+/**
+ * @} End of "defgroup field_permission_example".
+ */
+
+/**
+ * Implements hook_theme().
+ *
+ * Since we have a lot to explain, we're going to use Twig to do it.
+ */
+function field_permission_example_theme() {
+ return [
+ 'field_permission_description' => [
+ 'template' => 'description',
+ 'variables' => [
+ 'admin_link' => NULL,
+ ],
+ ],
+ ];
+}
diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.permissions.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.permissions.yml
new file mode 100644
index 000000000..c70eb2b35
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.permissions.yml
@@ -0,0 +1,11 @@
+# Permissions for the field_permission_example module
+'view own fieldnote':
+ title: View own fieldnote
+'edit own fieldnote':
+ title: Edit own fieldnote
+'view any fieldnote':
+ title: View any fieldnote
+'edit any fieldnote':
+ title: Edit any fieldnote
+'administer the fieldnote field':
+ title: Administer settings for the fieldnote field.
diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.routing.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.routing.yml
new file mode 100644
index 000000000..b58b97b47
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.routing.yml
@@ -0,0 +1,7 @@
+field_permission_example.description:
+ path: '/examples/field-permission-example'
+ defaults:
+ _title: 'Field Permission Example'
+ _controller: '\Drupal\field_permission_example\Controller\FieldPermissionExampleController::description'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Controller/FieldPermissionExampleController.php b/sites/all/modules/examples/examples/field_permission_example/src/Controller/FieldPermissionExampleController.php
new file mode 100644
index 000000000..2cca08d23
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/src/Controller/FieldPermissionExampleController.php
@@ -0,0 +1,28 @@
+t('the permissions admin page'), 'user.admin_permissions')->toString();
+
+ $build = [
+ 'description' => [
+ '#theme' => 'field_permission_description',
+ '#admin_link' => $permissions_admin_link,
+ ],
+ ];
+ return $build;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php
new file mode 100644
index 000000000..ce91470dd
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php
@@ -0,0 +1,49 @@
+ $item) {
+ $elements[$delta] = [
+ // We wrap the fieldnote content up in a div tag.
+ '#type' => 'html_tag',
+ '#tag' => 'div',
+ // This text is auto-XSS escaped. See docs for the html_tag element.
+ '#value' => $item->value,
+ // Let's give the note a nice sticky-note CSS appearance.
+ '#attributes' => [
+ 'class' => 'stickynote',
+ ],
+ // ..And this is the CSS for the stickynote.
+ '#attached' => [
+ 'library' => ['field_permission_example/fieldnote_sticky'],
+ ],
+ ];
+ }
+
+ return $elements;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php
new file mode 100644
index 000000000..0793e1691
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php
@@ -0,0 +1,55 @@
+ [
+ 'value' => [
+ 'type' => 'text',
+ 'size' => 'normal',
+ 'not null' => FALSE,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEmpty() {
+ $value = $this->get('value')->getValue();
+ return $value === NULL || $value === '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+ $properties['value'] = DataDefinition::create('string')
+ ->setLabel(t('Field Note'));
+
+ return $properties;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php
new file mode 100644
index 000000000..cd24f05df
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php
@@ -0,0 +1,35 @@
+value) ? $items[$delta]->value : '';
+ $element += [
+ '#type' => 'textarea',
+ '#default_value' => $value,
+ ];
+ return ['value' => $element];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/field_permission_example/templates/description.html.twig b/sites/all/modules/examples/examples/field_permission_example/templates/description.html.twig
new file mode 100644
index 000000000..1df2bc8dc
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/templates/description.html.twig
@@ -0,0 +1,28 @@
+{#
+/**
+ * @file
+ * Contains the text of the field_permission_example explanation/description page
+ *
+ * Available variables:
+ * - admin_link: The translated link pointing to the administer permissions page.
+ */
+#}
+
+
+{% trans %}
+
The Field Permission Example module shows how you can restrict view and edit permissions
+ within your field implementation. It adds a new field type called Fieldnote. Fieldnotes
+ appear as simple text boxes on the create/edit form, and as sticky notes when viewed.
+ By 'sticky note' we mean 'Post-It Note' but that's a trademarked term.
+
+
To see this field in action, add it to a content type or user profile. Go to the
+ permissions page ({{admin_link}}) and look at the 'Field Permission Example' section. This
+ allows you to change which roles can see and edit Fieldnote fields.
+
+
Creating different users with different capabilities will let you see these behaviors
+ in action. Fieldnote helpfully displays a message telling you which permissions it is
+ trying to resolve for the current field/user combination.
+
+
Definitely look through the code to see various implementation details.
+{% endtrans %}
+
diff --git a/sites/all/modules/examples/examples/field_permission_example/tests/src/Kernel/FieldNoteItemTest.php b/sites/all/modules/examples/examples/field_permission_example/tests/src/Kernel/FieldNoteItemTest.php
new file mode 100644
index 000000000..ad932a744
--- /dev/null
+++ b/sites/all/modules/examples/examples/field_permission_example/tests/src/Kernel/FieldNoteItemTest.php
@@ -0,0 +1,290 @@
+container->get('entity_type.manager');
+
+ // Set up our entity_type and user type for our new field:
+ $type_manager
+ ->getStorage('field_storage_config')
+ ->create([
+ 'field_name' => 'field_fieldnote',
+ 'entity_type' => 'entity_test',
+ 'type' => 'field_permission_example_fieldnote',
+ ])->save();
+
+ $type_manager
+ ->getStorage('field_config')
+ ->create([
+ 'entity_type' => 'entity_test',
+ 'field_name' => 'field_fieldnote',
+ 'bundle' => 'entity_test',
+ ])->save();
+
+ // Create a form display for the default form mode, and
+ // add our field type.
+ $type_manager
+ ->getStorage('entity_form_display')
+ ->create([
+ 'targetEntityType' => 'entity_test',
+ 'bundle' => 'entity_test',
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ])
+ ->setComponent('field_fieldnote', [
+ 'type' => 'field_permission_example_widget',
+ ])
+ ->save();
+
+ // Now do this for the user type.
+ $type_manager
+ ->getStorage('field_storage_config')
+ ->create([
+ 'field_name' => 'user_fieldnote',
+ 'entity_type' => 'user',
+ 'type' => 'field_permission_example_fieldnote',
+ ])->save();
+
+ $type_manager
+ ->getStorage('field_config')
+ ->create([
+ 'entity_type' => 'user',
+ 'field_name' => 'user_fieldnote',
+ 'bundle' => 'user',
+ ])->save();
+
+ // Fetch a form display for a user. This may already exist, so check as
+ // Core does.
+ // @see https://api.drupal.org/api/drupal/core%21includes%21entity.inc/function/entity_get_form_display/8
+ $entity_form_display
+ = $type_manager
+ ->getStorage('entity_form_display')
+ ->load('user.user.default');
+ if (empty($entity_form_display)) {
+ $entity_form_display
+ = $type_manager
+ ->getStorage('entity_form_display')
+ ->create([
+ 'targetEntityType' => 'user',
+ 'bundle' => 'user',
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ]);
+ }
+ // And add our fancy field to that display:
+ $entity_form_display->setComponent('field_fieldnote', [
+ 'type' => 'field_permission_example_widget',
+ ])->save();
+
+ }
+
+ /**
+ * Test entity fields of the field_permission_example_fieldnote field type.
+ */
+ public function testFieldNoteItem() {
+ // Verify entity creation.
+ $type_manager = $this->container->get('entity_type.manager');
+ $entity
+ = $type_manager
+ ->getStorage('entity_test')
+ ->create([]);
+ $value = 'This is an epic entity';
+ $entity->field_fieldnote = $value;
+ $entity->name->value = $this->randomMachineName();
+ $entity->save();
+
+ // Verify entity has been created properly.
+ $id = $entity->id();
+ $entity
+ = $type_manager
+ ->getStorage('entity_test')
+ ->load($id);
+
+ $this->assertTrue($entity->field_fieldnote instanceof FieldItemListInterface, 'Field implements interface.');
+ $this->assertTrue($entity->field_fieldnote[0] instanceof FieldItemInterface, 'Field item implements interface.');
+ $this->assertEqual($entity->field_fieldnote->value, $value);
+ $this->assertEqual($entity->field_fieldnote[0]->value, $value);
+
+ // Verify changing the field's value.
+ $new_value = $this->randomMachineName();
+ $entity->field_fieldnote->value = $new_value;
+ $this->assertEqual($entity->field_fieldnote->value, $new_value);
+
+ // Read changed entity and assert changed values.
+ $entity->save();
+
+ $entity
+ = $type_manager
+ ->getStorage('entity_test')
+ ->load($id);
+
+ $this->assertEqual($entity->field_fieldnote->value, $new_value);
+
+ // Test sample item generation.
+ $entity
+ = $type_manager
+ ->getStorage('entity_test')
+ ->create([]);
+
+ $entity->field_fieldnote->generateSampleItems();
+ $this->entityValidateAndSave($entity);
+ }
+
+ /**
+ * Test multiple access scenarios for the fieldnote field.
+ */
+ public function testFieldNoteAccess() {
+
+ // Let's set up some scenarios.
+ $scenarios = [
+ 'admin_type' => [
+ 'perms' => ['administer the fieldnote field'],
+ 'can_view_any' => TRUE,
+ 'can_edit_any' => TRUE,
+ 'can_view_own' => TRUE,
+ 'can_edit_own' => TRUE,
+ ],
+ 'low_access' => [
+ 'perms' => ['view test entity'],
+ 'can_view_any' => FALSE,
+ 'can_edit_any' => FALSE,
+ 'can_view_own' => FALSE,
+ 'can_edit_own' => FALSE,
+ ],
+ 'view_any' => [
+ 'perms' => [
+ 'view test entity',
+ 'view any fieldnote',
+ ],
+ 'can_view_any' => TRUE,
+ 'can_edit_any' => FALSE,
+ 'can_view_own' => FALSE,
+ 'can_edit_own' => FALSE,
+ ],
+ 'edit_any' => [
+ 'perms' => [
+ 'view test entity',
+ 'view any fieldnote',
+ 'edit any fieldnote',
+ ],
+ 'can_view_any' => TRUE,
+ 'can_edit_any' => TRUE,
+ 'can_view_own' => FALSE,
+ 'can_edit_own' => FALSE,
+ ],
+ 'view_own' => [
+ 'perms' => [
+ 'view test entity',
+ 'view own fieldnote',
+ ],
+ 'can_view_any' => FALSE,
+ 'can_edit_any' => FALSE,
+ 'can_view_own' => TRUE,
+ 'can_edit_own' => FALSE,
+ ],
+ 'edit_own' => [
+ 'perms' => [
+ 'view test entity',
+ 'view own fieldnote',
+ 'edit own fieldnote',
+ ],
+ 'can_view_any' => FALSE,
+ 'can_edit_any' => FALSE,
+ 'can_view_own' => TRUE,
+ 'can_edit_own' => TRUE,
+ ],
+ ];
+
+ $value = 'This is an epic entity';
+ // We also need to test users as an entity to attach to. They work
+ // a little differently than most content entity types:
+ $arbitrary_user = $this->createUser([], 'Some User');
+ $arbitrary_user->user_fieldnote = $value;
+ $arbitrary_user->save();
+
+ foreach ($scenarios as $name => $scenario) {
+ $test_user = $this->createUser($scenario['perms'], $name);
+ $entity = entity_create('entity_test');
+ $entity->field_fieldnote = $value;
+ $entity->name->value = $this->randomMachineName();
+ $entity->save();
+
+ foreach (['can_view_any', 'can_edit_any'] as $op) {
+ $this->doAccessAssertion($entity, 'field_fieldnote', $test_user, $name, $op, $scenario[$op]);
+ $this->doAccessAssertion($arbitrary_user, 'user_fieldnote', $test_user, $name, $op, $scenario[$op]);
+ }
+
+ if ($scenario['can_view_own'] or $scenario['can_edit_own']) {
+ $entity->user_id = $test_user;
+ $entity->save();
+ $test_user->user_fieldnote = $value;
+ $test_user->save();
+
+ foreach (['can_view_own', 'can_edit_own'] as $op) {
+ $this->doAccessAssertion($entity, 'field_fieldnote', $test_user, $name, $op, $scenario[$op]);
+ $this->doAccessAssertion($test_user, 'user_fieldnote', $test_user, $name, $op, $scenario[$op]);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Helper routine to run the assertions.
+ */
+ protected function doAccessAssertion($entity, $field_name, $account, $name, $op, $expected) {
+ $expect_str = $expected ? "CAN" : "CANNOT";
+ $assert_str = "$name $expect_str do $op on field $field_name";
+ $operation = preg_match('/edit/', $op) ? "edit" : "view";
+ $result = $entity->$field_name->access($operation, $account);
+ if ($expected) {
+ $this->assertTrue($result, $assert_str);
+ }
+ else {
+ $this->assertFalse($result, $assert_str);
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/file_example/file_example.info.yml b/sites/all/modules/examples/examples/file_example/file_example.info.yml
new file mode 100644
index 000000000..7248eb3b0
--- /dev/null
+++ b/sites/all/modules/examples/examples/file_example/file_example.info.yml
@@ -0,0 +1,15 @@
+name: File Example
+type: module
+description: Demonstrates using the Drupal File API and stream wrappers.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:file
+ - examples:examples
+ - examples:stream_wrapper_example
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/file_example/file_example.links.menu.yml b/sites/all/modules/examples/examples/file_example/file_example.links.menu.yml
new file mode 100644
index 000000000..cce6e579f
--- /dev/null
+++ b/sites/all/modules/examples/examples/file_example/file_example.links.menu.yml
@@ -0,0 +1,7 @@
+#
+# This link will appear in the "Tools" menu.
+#
+file_example.fileapi:
+ title: File Example
+ parent: file_example.description
+ route_name: file_example.fileapi
diff --git a/sites/all/modules/examples/examples/file_example/file_example.module b/sites/all/modules/examples/examples/file_example/file_example.module
new file mode 100644
index 000000000..ab2e9801b
--- /dev/null
+++ b/sites/all/modules/examples/examples/file_example/file_example.module
@@ -0,0 +1,97 @@
+getAccount();
+ if ($account->hasPermission($permission)) {
+ return [
+ 'Content-Type: text/plain',
+ ];
+ }
+ }
+}
+
+/**
+ * @} End of "defgroup file_example".
+ */
diff --git a/sites/all/modules/examples/examples/file_example/file_example.permissions.yml b/sites/all/modules/examples/examples/file_example/file_example.permissions.yml
new file mode 100644
index 000000000..9ab5c2a9f
--- /dev/null
+++ b/sites/all/modules/examples/examples/file_example/file_example.permissions.yml
@@ -0,0 +1,13 @@
+'use file example':
+ title: Use the examples in the File Example module.
+
+#
+# We use the following permissions in our hook_file_download implementation.
+# See file_example.module for details.
+#
+'read private files':
+ title: See private files in the File Example module demo.
+'read temporary files':
+ title: See temporary files in the File Example module demo.
+'read session files':
+ title: See session files in the File Example module demo.
diff --git a/sites/all/modules/examples/examples/file_example/file_example.routing.yml b/sites/all/modules/examples/examples/file_example/file_example.routing.yml
new file mode 100644
index 000000000..ed00fad07
--- /dev/null
+++ b/sites/all/modules/examples/examples/file_example/file_example.routing.yml
@@ -0,0 +1,8 @@
+# Main page for our example.
+file_example.fileapi:
+ path: '/examples/file_example'
+ defaults:
+ _form: '\Drupal\file_example\Form\FileExampleReadWriteForm'
+ _title: 'File Example: Use the File API to read/write a file'
+ requirements:
+ _permission: 'use file example'
diff --git a/sites/all/modules/examples/examples/file_example/src/Form/FileExampleReadWriteForm.php b/sites/all/modules/examples/examples/file_example/src/Form/FileExampleReadWriteForm.php
new file mode 100644
index 000000000..52a0590fa
--- /dev/null
+++ b/sites/all/modules/examples/examples/file_example/src/Form/FileExampleReadWriteForm.php
@@ -0,0 +1,861 @@
+state = $state;
+ $this->fileSystem = $file_system;
+ $this->moduleHandler = $module_handler;
+ $this->requestStack = $request_stack;
+ $this->streamWrapperManager = $stream_wrapper_manager;
+ $this->sessionSchemeEnabled = $this->moduleHandler->moduleExists('stream_wrapper_example');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $state = $container->get('state');
+ $file_system = $container->get('file_system');
+ $module_handler = $container->get('module_handler');
+ $request_stack = $container->get('request_stack');
+ $stream_wrapper_manager = $container->get('stream_wrapper_manager');
+ return new static($state, $file_system, $stream_wrapper_manager, $module_handler, $request_stack);
+ }
+
+ /**
+ * Returns a unique string identifying the form.
+ *
+ * @return string
+ * The unique string identifying the form.
+ */
+ public function getFormId() {
+ return 'file_example_readwrite';
+ }
+
+ /**
+ * Get the default file.
+ *
+ * This appears in the first block of the form.
+ *
+ * @return string
+ * The URI of the default file.
+ */
+ protected function getDefaultFile() {
+ $fall_back_value = $this->sessionSchemeEnabled ? 'session://drupal.txt' : 'public://drupal.txt';
+ $default_file = $this->state->get('file_example_default_file', $fall_back_value);
+ return $default_file;
+ }
+
+ /**
+ * Fetch a SessionWrapper object.
+ *
+ * This is used to change relevant attributes of the Session. This will return
+ * FALSE if the stream_wrapper_example is not enabled.
+ *
+ * @return Drupal\stream_wrapper_example\StreamWrapper\SessionWrapper|bool
+ * Wrapper object to manipulate the SESSION storage or FALSE if the session
+ * wrapper is unavailable.
+ */
+ protected function getSessionWrapper() {
+ if ($this->sessionSchemeEnabled) {
+ return new SessionWrapper($this->requestStack);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Set the default file.
+ *
+ * Set a default URI of the file used for read and write operations.
+ *
+ * @param string $uri
+ * URI to save for future display in the form.
+ */
+ protected function setDefaultFile($uri) {
+ $this->state->set('file_example_default_file', (string) $uri);
+ }
+
+ /**
+ * Get the default directory.
+ *
+ * @return string
+ * The URI of the default directory.
+ */
+ protected function getDefaultDirectory() {
+ $fall_back_value = $this->sessionSchemeEnabled ? 'session://directory1' : 'public://directory1';
+ $default_directory = $this->state->get('file_example_default_directory', $fall_back_value);
+ return $default_directory;
+ }
+
+ /**
+ * Set the default directory.
+ *
+ * @param string $uri
+ * URI to save for later form display.
+ */
+ protected function setDefaultDirectory($uri) {
+ $this->state->set('file_example_default_directory', (string) $uri);
+ }
+
+ /**
+ * Utility function to check for and return a managed file.
+ *
+ * In this demonstration code we don't necessarily know if a file is managed
+ * or not, so often need to check to do the correct behavior. Normal code
+ * would not have to do this, as it would be working with either managed or
+ * unmanaged files.
+ *
+ * @param string $uri
+ * The URI of the file, like public://test.txt.
+ *
+ * @return \Drupal\file\Entity\FileInterface|bool
+ * A file object that matches the URI, or FALSE if not a managed file.
+ *
+ * @todo This should still work. An entity query could be used instead.
+ * May be other alternatives.
+ */
+ private static function getManagedFile($uri) {
+ $fid = Database::getConnection('default')->query(
+ 'SELECT fid FROM {file_managed} WHERE uri = :uri',
+ [':uri' => $uri]
+ )->fetchField();
+ if (!empty($fid)) {
+ $file_object = File::load($fid);
+ return $file_object;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Prepare Url objects to prevent exceptions by the URL generator.
+ *
+ * Helper function to get us an external URL if this is legal, and to catch
+ * the exception Drupal throws if this is not possible.
+ *
+ * In Drupal 8, the URL generator is very sensitive to how you set things
+ * up, and some functions, in particular LinkGeneratorTrait::l(), will throw
+ * exceptions if you deviate from what's expected. This function will raise
+ * the chances your URL will be valid, and not do this.
+ *
+ * @param \Drupal\file\Entity\File|string $file_object
+ * A file entity object.
+ *
+ * @return \Drupal\Core\Url
+ * A Url object that can be displayed as an internal URL.
+ */
+ protected function getExternalUrl($file_object) {
+ if ($file_object instanceof FileInterface) {
+ $uri = $file_object->getFileUri();
+ }
+ else {
+ // A little tricky, since file.inc is a little inconsistent, but often
+ // this is a Uri.
+ $uri = file_create_url($file_object);
+ }
+
+ try {
+ // If we have been given a PHP stream URI, ask the stream itself if it
+ // knows how to create an external URL.
+ $wrapper = $this->streamWrapperManager->getViaUri($uri);
+ if ($wrapper) {
+ $external_url = $wrapper->getExternalUrl();
+ // Some streams may not have the concept of an external URL, so we
+ // check here to make sure, since the example assumes this.
+ if ($external_url) {
+ $url = Url::fromUri($external_url);
+ return $url;
+ }
+ }
+ else {
+ $url = Url::fromUri($uri);
+ // If we did not throw on ::fromUri (you can), we return the URL.
+ return $url;
+ }
+ }
+ catch (\Exception $e) {
+ return FALSE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $default_file = $this->getDefaultFile();
+ $default_directory = $this->getDefaultDirectory();
+
+ $form['description'] = [
+ '#markup' => $this->t('This form demonstrates the Drupal 8 file api. Experiment with the form, and then look at the submit handlers in the code to understand the file api.'),
+ ];
+
+ $form['write_file'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Write to a file'),
+ ];
+ $form['write_file']['write_contents'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Enter something you would like to write to a file'),
+ '#default_value' => $this->t('Put some text here or just use this text'),
+ ];
+
+ $form['write_file']['destination'] = [
+ '#type' => 'textfield',
+ '#default_value' => $default_file,
+ '#title' => $this->t('Optional: Enter the streamwrapper saying where it should be written'),
+ '#description' => $this->t('This may be public://some_dir/test_file.txt or private://another_dir/some_file.txt, for example. If you include a directory, it must already exist. The default is "public://". Since this example supports session://, you can also use something like session://somefile.txt.'),
+ ];
+
+ $form['write_file']['managed_submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Write managed file'),
+ '#submit' => ['::handleManagedFile'],
+ ];
+ $form['write_file']['unmanaged_submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Write unmanaged file'),
+ '#submit' => ['::handleUnmanagedFile'],
+ ];
+ $form['write_file']['unmanaged_php'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Unmanaged using PHP'),
+ '#submit' => ['::handleUnmanagedPhp'],
+ ];
+
+ $form['fileops'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Read from a file'),
+ ];
+ $form['fileops']['fileops_file'] = [
+ '#type' => 'textfield',
+ '#default_value' => $default_file,
+ '#title' => $this->t('Enter the URI of a file'),
+ '#description' => $this->t('This must be a stream-type description like public://some_file.txt or http://drupal.org or private://another_file.txt or (for this example) session://yet_another_file.txt.'),
+ ];
+ $form['fileops']['read_submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Read the file and store it locally'),
+ '#submit' => ['::handleFileRead'],
+ ];
+ $form['fileops']['delete_submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Delete file'),
+ '#submit' => ['::handleFileDelete'],
+ ];
+ $form['fileops']['check_submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Check to see if file exists'),
+ '#submit' => ['::handleFileExists'],
+ ];
+
+ $form['directory'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Create or prepare a directory'),
+ ];
+
+ $form['directory']['directory_name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Directory to create/prepare/delete'),
+ '#default_value' => $default_directory,
+ '#description' => $this->t('This is a directory as in public://some/directory or private://another/dir.'),
+ ];
+ $form['directory']['create_directory'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Create directory'),
+ '#submit' => ['::handleDirectoryCreate'],
+ ];
+ $form['directory']['delete_directory'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Delete directory'),
+ '#submit' => ['::handleDirectoryDelete'],
+ ];
+ $form['directory']['check_directory'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Check to see if directory exists'),
+ '#submit' => ['::handleDirectoryExists'],
+ ];
+
+ $form['debug'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Debugging'),
+ ];
+ // The Session Wrapper Exampple is not yet committed, so
+ // we hide this button until this happens.
+ $form['debug']['show_raw_session'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Show raw $_SESSION contents'),
+ '#submit' => ['::handleShowSession'],
+ '#access' => $this->sessionSchemeEnabled,
+ ];
+ $form['debug']['reset_session'] = [
+ '#type' => 'submit',
+ '#value' => t('Reset the Session'),
+ '#submit' => ['::handleResetSession'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Submit handler to write a managed file.
+ *
+ * A "managed file" is a file that Drupal tracks as a file entity. It's the
+ * standard way Drupal manages files in file fields and elsewhere.
+ *
+ * The key functions used here are:
+ * - file_save_data(), which takes a buffer and saves it to a named file and
+ * also creates a tracking record in the database and returns a file object.
+ * In this function we use FILE_EXISTS_RENAME (the default) as the argument,
+ * which means that if there's an existing file, create a new non-colliding
+ * filename and use it.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ * * @param array $form
+ * An associative array containing the structure of the form.
+ *
+ * @param array &$form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function handleManagedFile(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $data = $form_values['write_contents'];
+ $uri = !empty($form_values['destination']) ? $form_values['destination'] : NULL;
+
+ // Managed operations work with a file object.
+ $file_object = \file_save_data($data, $uri, FILE_EXISTS_RENAME);
+ if (!empty($file_object)) {
+ $url = $this->getExternalUrl($file_object);
+ $this->setDefaultFile($file_object->getFileUri());
+ $file_data = $file_object->toArray();
+ if ($url) {
+ drupal_set_message(
+ $this->t('Saved managed file: %file to destination %destination (accessible via this URL, actual uri=@uri)',
+ [
+ '%file' => print_r($file_data, TRUE),
+ '%destination' => $uri,
+ '@uri' => $file_object->getFileUri(),
+ ':url' => $url->toString(),
+ ]
+ )
+ );
+ }
+ else {
+ // This Uri is not routable, so we cannot give a link to it.
+ drupal_set_message(
+ $this->t('Saved managed file: %file to destination %destination (no URL, since this stream type does not support it)',
+ [
+ '%file' => print_r($file_data, TRUE),
+ '%destination' => $uri,
+ '@uri' => $file_object->getFileUri(),
+ ]
+ )
+ );
+
+ }
+ }
+ else {
+ drupal_set_message(t('Failed to save the managed file'), 'error');
+ }
+
+ }
+
+ /**
+ * Submit handler to write an unmanaged file.
+ *
+ * An unmanaged file is a file that Drupal does not track. A standard
+ * operating system file, in other words.
+ *
+ * The key functions used here are:
+ * - file_unmanaged_save_data(), which takes a buffer and saves it to a named
+ * file, but does not create any kind of tracking record in the database.
+ * This example uses FILE_EXISTS_REPLACE for the third argument, meaning
+ * that if there's an existing file at this location, it should be replaced.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ * * @param array $form
+ * An associative array containing the structure of the form.
+ *
+ * @param array &$form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function handleUnmanagedFile(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $data = $form_values['write_contents'];
+ $destination = !empty($form_values['destination']) ? $form_values['destination'] : NULL;
+
+ // With the unmanaged file we just get a filename back.
+ $filename = file_unmanaged_save_data($data, $destination, FILE_EXISTS_REPLACE);
+ if ($filename) {
+ $url = $this->getExternalUrl($filename);
+ $this->setDefaultFile($filename);
+ if ($url) {
+ drupal_set_message(
+ $this->t('Saved file as %filename (accessible via this URL, uri=@uri)',
+ [
+ '%filename' => $filename,
+ '@uri' => $filename,
+ ':url' => $url->toString(),
+ ]
+ )
+ );
+ }
+ else {
+ drupal_set_message(
+ $this->t('Saved file as %filename (not accessible externally)',
+ [
+ '%filename' => $filename,
+ '@uri' => $filename,
+ ]
+ )
+ );
+ }
+ }
+ else {
+ drupal_set_message(t('Failed to save the file'), 'error');
+ }
+ }
+
+ /**
+ * Submit handler to write an unmanaged file using plain PHP functions.
+ *
+ * The key functions used here are:
+ * - file_unmanaged_save_data(), which takes a buffer and saves it to a named
+ * file, but does not create any kind of tracking record in the database.
+ * - file_create_url(), which converts a URI in the form public://junk.txt or
+ * private://something/test.txt into a URL like
+ * http://example.com/sites/default/files/junk.txt.
+ * - drupal_tempnam() generates a temporary filename for use.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function handleUnmanagedPhp(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $data = $form_values['write_contents'];
+ $destination = !empty($form_values['destination']) ? $form_values['destination'] : NULL;
+
+ if (empty($destination)) {
+ // If no destination has been provided, use a generated name.
+ $destination = $this->fileSystem->tempnam('public://', 'file');
+ }
+
+ // With all traditional PHP functions we can use the stream wrapper notation
+ // for a file as well.
+ $fp = fopen($destination, 'w');
+
+ // To demonstrate the fact that everything is based on streams, we'll do
+ // multiple 5-character writes to put this to the file. We could easily
+ // (and far more conveniently) write it in a single statement with
+ // fwrite($fp, $data).
+ $length = strlen($data);
+ $write_size = 5;
+ for ($i = 0; $i < $length; $i += $write_size) {
+ $result = fwrite($fp, substr($data, $i, $write_size));
+ if ($result === FALSE) {
+ drupal_set_message(t('Failed writing to the file %file', ['%file' => $destination]), 'error');
+ fclose($fp);
+ return;
+ }
+ }
+ $url = $this->getExternalUrl($destination);
+ $this->setDefaultFile($destination);
+ if ($url) {
+ drupal_set_message(
+ $this->t('Saved file as %filename (accessible via this URL, uri=@uri)',
+ [
+ '%filename' => $destination,
+ '@uri' => $destination,
+ ':url' => $url->toString(),
+ ]
+ )
+ );
+ }
+ else {
+ drupal_set_message(
+ $this->t('Saved file as %filename (not accessible externally)',
+ [
+ '%filename' => $destination,
+ '@uri' => $destination,
+ ]
+ )
+ );
+ }
+
+ }
+
+ /**
+ * Submit handler for reading a stream wrapper.
+ *
+ * Drupal now has full support for PHP's stream wrappers, which means that
+ * instead of the traditional use of all the file functions
+ * ($fp = fopen("/tmp/some_file.txt");) far more sophisticated and generalized
+ * (and extensible) things can be opened as if they were files. Drupal itself
+ * provides the public:// and private:// schemes for handling public and
+ * private files. PHP provides file:// (the default) and http://, so that a
+ * URL can be read or written (as in a POST) as if it were a file. In
+ * addition, new schemes can be provided for custom applications. The Stream
+ * Wrapper Example, if installed, impleents a custom 'session' scheme that
+ * you can test with this example.
+ *
+ * Here we take the stream wrapper provided in the form. We grab the
+ * contents with file_get_contents(). Notice that's it's as simple as that:
+ * file_get_contents("http://example.com") or
+ * file_get_contents("public://somefile.txt") just works. Although it's
+ * not necessary, we use file_unmanaged_save_data() to save this file locally
+ * and then find a local URL for it by using file_create_url().
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function handleFileRead(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $uri = $form_values['fileops_file'];
+
+ if (empty($uri) or !is_file($uri)) {
+ drupal_set_message(t('The file "%uri" does not exist', ['%uri' => $uri]), 'error');
+ return;
+ }
+
+ // Make a working filename to save this by stripping off the (possible)
+ // file portion of the streamwrapper. If it's an evil file extension,
+ // file_munge_filename() will neuter it.
+ $filename = file_munge_filename(preg_replace('@^.*/@', '', $uri), '', TRUE);
+ $buffer = file_get_contents($uri);
+
+ if ($buffer) {
+ $sourcename = file_unmanaged_save_data($buffer, 'public://' . $filename);
+ if ($sourcename) {
+ $url = $this->getExternalUrl($sourcename);
+ $this->setDefaultFile($sourcename);
+ if ($url) {
+ drupal_set_message(
+ $this->t('The file was read and copied to %filename which is accessible at this URL',
+ [
+ '%filename' => $sourcename,
+ ':url' => $url->toString(),
+ ]
+ )
+ );
+ }
+ else {
+ drupal_set_message(
+ $this->t('The file was read and copied to %filename (not accessible externally)',
+ [
+ '%filename' => $sourcename,
+ ]
+ )
+ );
+
+ }
+ }
+ else {
+ drupal_set_message(t('Failed to save the file'));
+ }
+ }
+ else {
+ // We failed to get the contents of the requested file.
+ drupal_set_message(t('Failed to retrieve the file %file', ['%file' => $uri]));
+ }
+
+ }
+
+ /**
+ * Submit handler to delete a file.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function handleFileDelete(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $uri = $form_values['fileops_file'];
+
+ // Since we don't know if the file is managed or not, look in the database
+ // to see. Normally, code would be working with either managed or unmanaged
+ // files, so this is not a typical situation.
+ $file_object = self::getManagedFile($uri);
+
+ // If a managed file, use file_delete().
+ if (!empty($file_object)) {
+ // While file_delete should return FALSE on failure,
+ // it can currently throw an exception on certain cache states.
+ try {
+ // This no longer returns a result code. If things go bad,
+ // it will throw an exception:
+ file_delete($file_object->id());
+ drupal_set_message(t('Successfully deleted managed file %uri', ['%uri' => $uri]));
+ $this->setDefaultFile($uri);
+ }
+ catch (\Exception $e) {
+ drupal_set_message(t('Failed deleting managed file %uri. Result was %result',
+ [
+ '%uri' => $uri,
+ '%result' => print_r($e->getMessage(), TRUE),
+ ]
+ ), 'error');
+ }
+ }
+ // Else use file_unmanaged_delete().
+ else {
+ $result = file_unmanaged_delete($uri);
+ if ($result !== TRUE) {
+ drupal_set_message(t('Failed deleting unmanaged file %uri', ['%uri' => $uri, 'error']));
+ }
+ else {
+ drupal_set_message(t('Successfully deleted unmanaged file %uri', ['%uri' => $uri]));
+ $this->setDefaultFile('file_example_default_file', $uri);
+ }
+ }
+ }
+
+ /**
+ * Submit handler to check existence of a file.
+ */
+ public function handleFileExists(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $uri = $form_values['fileops_file'];
+ if (is_file($uri)) {
+ drupal_set_message(t('The file %uri exists.', ['%uri' => $uri]));
+ }
+ else {
+ drupal_set_message(t('The file %uri does not exist.', ['%uri' => $uri]));
+ }
+ }
+
+ /**
+ * Submit handler for directory creation.
+ *
+ * Here we create a directory and set proper permissions on it using
+ * file_prepare_directory().
+ */
+ public function handleDirectoryCreate(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $directory = $form_values['directory_name'];
+
+ // The options passed to file_prepare_directory are a bitmask, so we can
+ // specify either FILE_MODIFY_PERMISSIONS (set permissions on the
+ // directory), FILE_CREATE_DIRECTORY, or both together:
+ // FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY.
+ // FILE_MODIFY_PERMISSIONS will set the permissions of the directory by
+ // by default to 0755, or to the value of the variable
+ // 'file_chmod_directory'.
+ if (!file_prepare_directory($directory, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY)) {
+ drupal_set_message(t('Failed to create %directory.', ['%directory' => $directory]), 'error');
+ }
+ else {
+ $result = is_dir($directory);
+ drupal_set_message(t('Directory %directory is ready for use.', ['%directory' => $directory]));
+ $this->setDefaultDirectory($directory);
+ }
+ }
+
+ /**
+ * Submit handler for directory deletion.
+ *
+ * @see file_unmanaged_delete_recursive()
+ */
+ public function handleDirectoryDelete(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $directory = $form_values['directory_name'];
+
+ $result = file_unmanaged_delete_recursive($directory);
+ if (!$result) {
+ drupal_set_message(t('Failed to delete %directory.', ['%directory' => $directory]), 'error');
+ }
+ else {
+ drupal_set_message(t('Recursively deleted directory %directory.', ['%directory' => $directory]));
+ $this->setDefaultDirectory($directory);
+ }
+ }
+
+ /**
+ * Submit handler to test directory existence.
+ *
+ * This actually just checks to see if the directory is writable.
+ *
+ * @param array $form
+ * FormAPI form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * FormAPI form state.
+ */
+ public function handleDirectoryExists(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ $directory = $form_values['directory_name'];
+ $result = is_dir($directory);
+ if (!$result) {
+ drupal_set_message(t('Directory %directory does not exist.', ['%directory' => $directory]));
+ }
+ else {
+ drupal_set_message(t('Directory %directory exists.', ['%directory' => $directory]));
+ }
+ }
+
+ /**
+ * Utility submit function to show the contents of $_SESSION.
+ */
+ public function handleShowSession(array &$form, FormStateInterface $form_state) {
+ $form_values = $form_state->getValues();
+ // If the devel module is installed, use it's nicer message format.
+ if ($this->moduleHandler->moduleExists('devel')) {
+ // @codingStandardsIgnoreStart
+ // We wrap this in the coding standards ignore tags because the use of
+ // function dsm() is discouraged.
+ dsm($this->getStoredData(), $this->t('Entire $_SESSION["file_example"]'));
+ // @codingStandardsIgnoreEnd
+ }
+ else {
+ drupal_set_message('
' . print_r($this->getStoredData(), TRUE) . '
');
+ }
+ }
+
+ /**
+ * Utility submit function to reset the demo.
+ *
+ * @param array $form
+ * FormAPI form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * FormAPI form state.
+ *
+ * @todo Note this does NOT clear any managed file references in Drupal's DB.
+ * It might be a good idea to add this.
+ */
+ public function handleResetSession(array &$form, FormStateInterface $form_state) {
+ $this->state->delete('file_example_default_file');
+ $this->state->delete('file_example_default_directory');
+ $this->clearStoredData();
+ drupal_set_message('Session reset.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ // We don't use this, but the interface requires us to implement it.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // We don't use this, but the interface requires us to implement it.
+ }
+
+ /**
+ * Get our stored data for display.
+ */
+ protected function getStoredData() {
+ $handle = $this->getSessionWrapper();
+ if ($handle) {
+ return $handle->getPath('');
+ }
+ return "SESSION STORE IS NOT ENABLED";
+ }
+
+ /**
+ * Reset our stored data.
+ */
+ protected function clearStoredData() {
+ $handle = $this->getSessionWrapper();
+ if ($handle) {
+ return $handle->cleanUpStore();
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/file_example/tests/src/Functional/FileExampleTest.php b/sites/all/modules/examples/examples/file_example/tests/src/Functional/FileExampleTest.php
new file mode 100644
index 000000000..0383193c9
--- /dev/null
+++ b/sites/all/modules/examples/examples/file_example/tests/src/Functional/FileExampleTest.php
@@ -0,0 +1,152 @@
+assertSession();
+
+ // Our test user needs to access some non-standard file types,
+ // so we bless it accordingly.
+ $permissions = [
+ 'use file example',
+ 'read private files',
+ 'read temporary files',
+ 'read session files',
+ ];
+ $priviledged_user = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($priviledged_user);
+
+ $expected_text = [
+ 'Write managed file' => 'Saved managed file',
+ 'Write unmanaged file' => 'Saved file as',
+ 'Unmanaged using PHP' => 'Saved file as',
+ ];
+ // For each of the three buttons == three write types.
+ $buttons = [
+ 'Write managed file',
+ 'Write unmanaged file',
+ 'Unmanaged using PHP',
+ ];
+ foreach ($buttons as $button) {
+ // For each scheme supported by Drupal + the session:// wrapper,
+ // which is defined in the stream_wrapper_exampnle.
+ $schemes = ['public', 'private', 'temporary', 'session'];
+ foreach ($schemes as $scheme) {
+ // Create a directory for use.
+ $dirname = $scheme . '://' . $this->randomMachineName(10);
+
+ // Directory does not yet exist; assert that.
+ $edit = [
+ 'directory_name' => $dirname,
+ ];
+ $this->drupalPostForm('examples/file_example', $edit, 'Check to see if directory exists');
+ $assert->pageTextContains(t('Directory @dirname does not exist', ['@dirname' => $dirname]));
+
+ $this->drupalPostForm('examples/file_example', $edit, 'Create directory');
+ $assert->pageTextContains(t('Directory @dirname is ready for use', ['@dirname' => $dirname]));
+
+ $this->drupalPostForm('examples/file_example', $edit, 'Check to see if directory exists');
+ $assert->pageTextContains(t('Directory @dirname exists', ['@dirname' => $dirname]));
+
+ // Create a file in the directory we created.
+ $content = $this->randomMachineName(30);
+ $filename = $dirname . '/' . $this->randomMachineName(30) . '.txt';
+
+ // Assert that the file we're about to create does not yet exist.
+ $edit = [
+ 'fileops_file' => $filename,
+ ];
+ $this->drupalPostForm('examples/file_example', $edit, 'Check to see if file exists');
+ $assert->pageTextContains(t('The file @filename does not exist', ['@filename' => $filename]));
+
+ $this->verbose("Processing button=$button, scheme=$scheme, dir=$dirname, file=$filename");
+ $edit = [
+ 'write_contents' => $content,
+ 'destination' => $filename,
+ ];
+ $this->drupalPostForm('examples/file_example', $edit, $button);
+ $this->verbose($expected_text[$button], "Button Text");
+ $assert->pageTextContains($expected_text[$button]);
+
+ // Capture the name of the output file, as it might have changed due
+ // to file renaming.
+ $element = $this->xpath('//span[@id="uri"]');
+ $output_filename = (string) $element[0]->getText();
+ $this->verbose($output_filename, 'Name of output file');
+
+ // Click the link provided that is an easy way to get the data for
+ // checking and make sure that the data we put in is what we get out.
+ if (!in_array($scheme, [])) {
+ $this->clickLink(t('this URL'));
+ $assert->statusCodeEquals(200);
+ // assertText give sketchy answers when the content is *exactly* the
+ // contents of the buffer, so let's do something less fragile.
+ // $this->assertText($content);
+ $buffer = $this->getSession()->getPage()->getContent();
+ $this->assertEquals($content, $buffer);
+ }
+
+ // Verify that the file exists.
+ $edit = [
+ 'fileops_file' => $filename,
+ ];
+ $this->drupalPostForm('examples/file_example', $edit, 'Check to see if file exists');
+ $assert->pageTextContains("The file $filename exists");
+
+ // Now read the file that got written above and verify that we can use
+ // the writing tools.
+ $edit = [
+ 'fileops_file' => $output_filename,
+ ];
+ $this->drupalPostForm('examples/file_example', $edit, 'Read the file and store it locally');
+
+ $assert->pageTextContains('The file was read and copied');
+
+ $edit = [
+ 'fileops_file' => $filename,
+ ];
+
+ $this->drupalPostForm('examples/file_example', $edit, 'Delete file');
+ $assert->pageTextContains('Successfully deleted');
+ $this->drupalPostForm('examples/file_example', $edit, 'Check to see if file exists');
+ $assert->pageTextContains(t('The file @filename does not exist', ['@filename' => $filename]));
+
+ $edit = [
+ 'directory_name' => $dirname,
+ ];
+ $this->drupalPostForm('examples/file_example', $edit, 'Delete directory');
+ $this->drupalPostForm('examples/file_example', $edit, 'Check to see if directory exists');
+ $assert->pageTextContains(t('Directory @dirname does not exist', ['@dirname' => $dirname]));
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.info.yml b/sites/all/modules/examples/examples/form_api_example/form_api_example.info.yml
new file mode 100644
index 000000000..f43aa205a
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.info.yml
@@ -0,0 +1,15 @@
+name: Form API Example
+type: module
+description: Demonstrates the Drupal Form API.
+package: Example modules
+# core: 8.x
+configure: form_api_example.description
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.links.menu.yml b/sites/all/modules/examples/examples/form_api_example/form_api_example.links.menu.yml
new file mode 100644
index 000000000..7be1d7569
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.links.menu.yml
@@ -0,0 +1,78 @@
+# Define default links for this module.
+form_api_example.description:
+ title: Form API Examples
+ description: Form examples using Drupal Form API.
+ route_name: form_api_example.description
+ expanded: TRUE
+
+form_api_example.simple_form:
+ title: Simple form
+ description: A simple form example with submit processing.
+ route_name: form_api_example.simple_form
+ parent: form_api_example.description
+ weight: -9
+
+form_api_example.multistep_form:
+ title: Multistep form
+ description: Multistep form example with submit processing.
+ route_name: form_api_example.multistep_form
+ parent: form_api_example.description
+ weight: -8
+
+form_api_example.input_demo:
+ title: Common input elements
+ description: A form to demonstrate input elements.
+ route_name: form_api_example.input_demo
+ parent: form_api_example.description
+ weight: -7
+
+form_api_example.build_demo:
+ title: Build form demo
+ description: Demonstrates the order of firing of from controller methods.
+ route_name: form_api_example.build_demo
+ parent: form_api_example.description
+ weight: -6
+
+form_api_example.container_demo:
+ title: Container elements
+ description: A form to demonstrate use of containers.
+ route_name: form_api_example.container_demo
+ parent: form_api_example.description
+ weight: -5
+
+form_api_example.state_demo:
+ title: Form state binding
+ description: A form to demonstrate binding of form state.
+ route_name: form_api_example.state_demo
+ parent: form_api_example.description
+ weight: -4
+
+form_api_example.vertical_tabs_demo:
+ title: Vertical tab elements
+ description: A form to demonstrate use of vertical tabs.
+ route_name: form_api_example.vertical_tabs_demo
+ parent: form_api_example.description
+ weight: -3
+
+form_api_example.ajax_color_demo:
+ title: Ajax form
+ description: AJAX form demo.
+ route_name: form_api_example.ajax_color_demo
+ parent: form_api_example.description
+ weight: -2
+
+form_api_example.ajax_addmore:
+ title: Add-more button
+ description: Form with 'add more' and 'remove' buttons.
+ route_name: form_api_example.ajax_addmore
+ parent: form_api_example.description
+ weight: -1
+
+form_api_example.modal_form:
+ title: Modal form
+ description: Demonstrates using Drupal's modal form.
+ route_name: form_api_example.modal_form
+ route_parameters:
+ nojs: nojs
+ parent: form_api_example.description
+ weight: 0
diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.module b/sites/all/modules/examples/examples/form_api_example/form_api_example.module
new file mode 100644
index 000000000..14f421041
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.module
@@ -0,0 +1,43 @@
+getForm()
+ * - form_api_example.routing.yml
+ *
+ * In addition to the simple example described above this module provides
+ * examples that demonstrate:
+ * - Elements used for data input
+ * -\Drupal\form_api_example\Form\InputDemo
+ * - The order of execution of form controller methods
+ * - \Drupal\form_api_example\Form\BuildDemo
+ * - Container elements used to group items
+ * - \Drupal\form_api_example\Form\ContainerDemo
+ * - Hiding elements based on the state of other elements
+ * - \Drupal\form_api_example\Form\StateDemo
+ * - Vertical Tabs elements in forms
+ * - \Drupal\form_api_example\Form\VerticalTabsDemo
+ * - Populating a portion on a form with Ajax callbacks
+ * - \Drupal\form_api_example\Form\AjaxColorForm
+ * - Adding additional fields to a form with Ajax callbacks
+ * - \Drupal\form_api_example\Form\AjaxAddMore
+ * - Modal form creation
+ * - \Drupal\form_api_example\Controller\Page
+ * - \Drupal\form_api_example\Form\ModalForm
+ *
+ * @} End of "defgroup field_example".
+ */
diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.routing.yml b/sites/all/modules/examples/examples/form_api_example/form_api_example.routing.yml
new file mode 100644
index 000000000..7cf20e348
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.routing.yml
@@ -0,0 +1,100 @@
+# This routing.yml file makes both the fapi example description page and the
+# included sample forms available at specific URL's on your site. A route
+# maps a URL path to a controller. For page controllers it defines the
+# function or method that will be called when the page is accessed. For form
+# controllers the content is determined by the buildForm method defined by the
+# form controller implementation.
+
+# Access to these paths is restricted to users with the permission
+# 'access content'. This is notated as _permission: 'access content'.
+
+# Menu items corresponding to these URLs are defined separately in the
+# form_api_example.links.menu.yml file.
+form_api_example.description:
+ path: 'examples/form-api-example'
+ defaults:
+ _controller: '\Drupal\form_api_example\Controller\Page::description'
+ _title: 'Form API Examples'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.simple_form:
+ path: 'examples/form-api-example/simple-form'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\SimpleForm'
+ _title: 'Simple form'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.input_demo:
+ path: 'examples/form-api-example/input-demo'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\InputDemo'
+ _title: 'Common input elements'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.state_demo:
+ path: 'examples/form-api-example/state-demo'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\StateDemo'
+ _title: 'Form state binding'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.container_demo:
+ path: 'examples/form-api-example/container-demo'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\ContainerDemo'
+ _title: 'Container elements'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.vertical_tabs_demo:
+ path: 'examples/form-api-example/vertical-tabs-demo'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\VerticalTabsDemo'
+ _title: 'Vertical tab elements'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.modal_form:
+ path: 'examples/form-api-example/modal-form/{nojs}'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\ModalForm'
+ _title: 'Modal form'
+ nojs: ajax
+ requirements:
+ _permission: 'access content'
+
+form_api_example.ajax_color_demo:
+ path: 'examples/form-api-example/ajax-color-demo'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\AjaxColorForm'
+ _title: 'AJAX color form'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.build_demo:
+ path: 'examples/form-api-example/build-demo'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\BuildDemo'
+ _title: 'Build form demo'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.ajax_addmore:
+ path: 'examples/form-api-example/ajax-addmore'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\AjaxAddMore'
+ _title: 'Add-more button'
+ requirements:
+ _permission: 'access content'
+
+form_api_example.multistep_form:
+ path: 'examples/form-api-example/multistep-form'
+ defaults:
+ _form: '\Drupal\form_api_example\Form\MultistepForm'
+ _title: 'Multistep form'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Controller/Page.php b/sites/all/modules/examples/examples/form_api_example/src/Controller/Page.php
new file mode 100644
index 000000000..c7b24a947
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Controller/Page.php
@@ -0,0 +1,21 @@
+ 'item',
+ '#markup' => $this->t('This example shows an add-more and a remove-last button.'),
+ ];
+
+ // Gather the number of names in the form already.
+ $num_names = $form_state->get('num_names');
+ // We have to ensure that there is at least one name field.
+ if ($num_names === NULL) {
+ $name_field = $form_state->set('num_names', 1);
+ $num_names = 1;
+ }
+
+ $form['#tree'] = TRUE;
+ $form['names_fieldset'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('People coming to picnic'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ ];
+
+ for ($i = 0; $i < $num_names; $i++) {
+ $form['names_fieldset']['name'][$i] = [
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ ];
+ }
+
+ $form['names_fieldset']['actions'] = [
+ '#type' => 'actions',
+ ];
+ $form['names_fieldset']['actions']['add_name'] = [
+ '#type' => 'submit',
+ '#value' => t('Add one more'),
+ '#submit' => ['::addOne'],
+ '#ajax' => [
+ 'callback' => '::addmoreCallback',
+ 'wrapper' => 'names-fieldset-wrapper',
+ ],
+ ];
+ // If there is more than one name, add the remove button.
+ if ($num_names > 1) {
+ $form['names_fieldset']['actions']['remove_name'] = [
+ '#type' => 'submit',
+ '#value' => t('Remove one'),
+ '#submit' => ['::removeCallback'],
+ '#ajax' => [
+ 'callback' => '::addmoreCallback',
+ 'wrapper' => 'names-fieldset-wrapper',
+ ],
+ ];
+ }
+ $form_state->setCached(FALSE);
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'form_api_example_ajax_addmore';
+ }
+
+ /**
+ * Callback for both ajax-enabled buttons.
+ *
+ * Selects and returns the fieldset with the names in it.
+ */
+ public function addmoreCallback(array &$form, FormStateInterface $form_state) {
+ $name_field = $form_state->get('num_names');
+ return $form['names_fieldset'];
+ }
+
+ /**
+ * Submit handler for the "add-one-more" button.
+ *
+ * Increments the max counter and causes a rebuild.
+ */
+ public function addOne(array &$form, FormStateInterface $form_state) {
+ $name_field = $form_state->get('num_names');
+ $add_button = $name_field + 1;
+ $form_state->set('num_names', $add_button);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Submit handler for the "remove one" button.
+ *
+ * Decrements the max counter and causes a form rebuild.
+ */
+ public function removeCallback(array &$form, FormStateInterface $form_state) {
+ $name_field = $form_state->get('num_names');
+ if ($name_field > 1) {
+ $remove_button = $name_field - 1;
+ $form_state->set('num_names', $remove_button);
+ }
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Final submit handler.
+ *
+ * Reports what values were finally set.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValue(['names_fieldset', 'name']);
+
+ $output = t('These people are coming to the picnic: @names', [
+ '@names' => implode(', ', $values),
+ ]
+ );
+ drupal_set_message($output);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/AjaxColorForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/AjaxColorForm.php
new file mode 100644
index 000000000..5861a14ee
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/AjaxColorForm.php
@@ -0,0 +1,147 @@
+ 'item',
+ '#markup' => $this->t('This form example demonstrates functioning of an AJAX callback.'),
+ ];
+
+ // The #ajax attribute used in the temperature input element defines an ajax
+ // callback that will invoke the 'updateColor' method on this form object.
+ // Whenever the temperature element changes, it will invoke this callback
+ // and replace the contents of the 'color_wrapper' container with the
+ // results of this method call.
+ $form['temperature'] = [
+ '#title' => $this->t('Temperature'),
+ '#type' => 'select',
+ '#options' => $this->getColorTemperatures(),
+ '#empty_option' => $this->t('- Select a color temperature -'),
+ '#ajax' => [
+ // Could also use [get_class($this), 'updateColor'].
+ 'callback' => '::updateColor',
+ 'wrapper' => 'color-wrapper',
+ ],
+ ];
+
+ // Add a wrapper that can be replaced with new HTML by the ajax callback.
+ // This is given the ID that was passed to the ajax callback in the '#ajax'
+ // element above.
+ $form['color_wrapper'] = [
+ '#type' => 'container',
+ '#attributes' => ['id' => 'color-wrapper'],
+ ];
+
+ // Add a color element to the color_wrapper container using the value
+ // from temperature to determine which colors to include in the select
+ // element.
+ $temperature = $form_state->getValue('temperature');
+ if (!empty($temperature)) {
+ $form['color_wrapper']['color'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Color'),
+ '#options' => $this->getColorsByTemperature($temperature),
+ ];
+ }
+
+ // Add a submit button that handles the submission of the form.
+ $form['actions'] = [
+ '#type' => 'actions',
+ 'submit' => [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Ajax callback for the color dropdown.
+ */
+ public function updateColor(array $form, FormStateInterface $form_state) {
+ return $form['color_wrapper'];
+ }
+
+ /**
+ * Returns colors that correspond with the given temperature.
+ *
+ * @param string $temperature
+ * The color temperature for which to return a list of colors. Can be either
+ * 'warm' or 'cool'.
+ *
+ * @return array
+ * An associative array of colors that correspond to the given color
+ * temperature, suitable to use as form options.
+ */
+ protected function getColorsByTemperature($temperature) {
+ return $this->getColors()[$temperature]['colors'];
+ }
+
+ /**
+ * Returns a list of color temperatures.
+ *
+ * @return array
+ * An associative array of color temperatures, suitable to use as form
+ * options.
+ */
+ protected function getColorTemperatures() {
+ return array_map(function ($color_data) {
+ return $color_data['name'];
+ }, $this->getColors());
+ }
+
+ /**
+ * Returns an array of colors grouped by color temperature.
+ *
+ * @return array
+ * An associative array of color data, keyed by color temperature.
+ */
+ protected function getColors() {
+ return [
+ 'warm' => [
+ 'name' => $this->t('Warm'),
+ 'colors' => [
+ 'red' => $this->t('Red'),
+ 'orange' => $this->t('Orange'),
+ 'yellow' => $this->t('Yellow'),
+ ],
+ ],
+ 'cool' => [
+ 'name' => $this->t('Cool'),
+ 'colors' => [
+ 'blue' => $this->t('Blue'),
+ 'purple' => $this->t('Purple'),
+ 'green' => $this->t('Green'),
+ ],
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/BuildDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/BuildDemo.php
new file mode 100644
index 000000000..5dfe16fe4
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/BuildDemo.php
@@ -0,0 +1,158 @@
+displayMethodInvocation('__construct');
+ }
+
+ /**
+ * Update form processing information.
+ *
+ * Display the method being called and it's sequence in the form
+ * processing.
+ *
+ * @param string $method_name
+ * The method being invoked.
+ */
+ private function displayMethodInvocation($method_name) {
+ self::$sequenceCounter++;
+ drupal_set_message(self::$sequenceCounter . ". $method_name");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+
+ $form['description'] = [
+ '#type' => 'item',
+ '#markup' => $this->t('Demonstrates how submit, rebuild, form-rebuild and #ajax submit work.'),
+ ];
+
+ // Simple checkbox for ajax orders.
+ $form['change'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Change Me'),
+ '#ajax' => [
+ 'callback' => '::ajaxSubmit',
+ 'wrapper' => 'message-wrapper',
+ ],
+ ];
+
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+
+ // Add a submit button that handles the submission of the form.
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ ];
+
+ // Add button handlers.
+ $form['actions']['button'] = [
+ '#type' => 'button',
+ '#value' => 'Rebuild',
+ ];
+
+ $form['actions']['rebuild'] = [
+ '#type' => 'submit',
+ '#value' => 'Submit Rebuild',
+ '#submit' => ['::rebuildFormSubmit'],
+ ];
+
+ $form['actions']['ajaxsubmit'] = [
+ '#type' => 'submit',
+ '#value' => 'Ajax Submit',
+ '#ajax' => [
+ 'callback' => '::ajaxSubmit',
+ 'wrapper' => 'message-wrapper',
+ ],
+ ];
+
+ $form['messages'] = [
+ '#type' => 'container',
+ '#attributes' => ['id' => 'message-wrapper'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ $this->displayMethodInvocation('getFormId');
+ return 'form_api_example_simple_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $this->displayMethodInvocation('validateForm');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->displayMethodInvocation('submitForm');
+ }
+
+ /**
+ * Implements ajax submit callback.
+ *
+ * @param array $form
+ * Form render array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Current state of the form.
+ */
+ public function ajaxSubmit(array &$form, FormStateInterface $form_state) {
+ $this->displayMethodInvocation('ajaxSubmit');
+ $form['messages']['status'] = [
+ '#type' => 'status_messages',
+ ];
+
+ return $form['messages'];
+ }
+
+ /**
+ * Implements submit callback for Rebuild button.
+ *
+ * @param array $form
+ * Form render array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Current state of the form.
+ */
+ public function rebuildFormSubmit(array &$form, FormStateInterface $form_state) {
+ $this->displayMethodInvocation('rebuildFormSubmit');
+ $form_state->setRebuild(TRUE);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/ContainerDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/ContainerDemo.php
new file mode 100644
index 000000000..503351916
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/ContainerDemo.php
@@ -0,0 +1,95 @@
+ 'item',
+ '#markup' => $this->t('This form example demonstrates container elements: details, fieldset and container.'),
+ ];
+
+ // Details containers replace D7's collapsible field sets.
+ $form['author'] = [
+ '#type' => 'details',
+ '#title' => 'Author Info (type = details)',
+ ];
+
+ $form['author']['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Name'),
+ ];
+
+ $form['author']['pen_name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Pen Name'),
+ ];
+
+ // Conventional field set.
+ $form['book'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Book Info (type = fieldset)'),
+ ];
+
+ $form['book']['title'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title'),
+ ];
+
+ $form['book']['publisher'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Publisher'),
+ ];
+
+ // Containers have no visual display but wrap any contained elements in a
+ // div tag.
+ $form['accommodation'] = [
+ '#type' => 'container',
+ ];
+
+ $form['accommodation']['title'] = [
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => $this->t('Special Accommodations (type = container)'),
+ ];
+
+ $form['accommodation']['diet'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Dietary Restrictions'),
+ ];
+
+ $form['actions'] = ['#type' => 'actions'];
+
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'form_api_example_container_demo';
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/DemoBase.php b/sites/all/modules/examples/examples/form_api_example/src/Form/DemoBase.php
new file mode 100644
index 000000000..6c2926541
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/DemoBase.php
@@ -0,0 +1,46 @@
+getValues();
+ foreach ($values as $key => $value) {
+ $label = isset($form[$key]['#title']) ? $form[$key]['#title'] : $key;
+
+ // Many arrays return 0 for unselected values so lets filter that out.
+ if (is_array($value)) {
+ $value = array_filter($value);
+ }
+ // Only display for controls that have titles and values.
+ if ($value) {
+ $display_value = is_array($value) ? print_r($value, 1) : $value;
+ $message = $this->t('Value for %title: %value', ['%title' => $label, '%value' => $display_value]);
+ drupal_set_message($message);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/InputDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/InputDemo.php
new file mode 100644
index 000000000..b841ac356
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/InputDemo.php
@@ -0,0 +1,325 @@
+ 'item',
+ '#markup' => $this->t('This example shows the use of all input-types.'),
+ ];
+
+ // CheckBoxes.
+ $form['tests_taken'] = [
+ '#type' => 'checkboxes',
+ '#options' => ['SAT' => t('SAT'), 'ACT' => t('ACT')],
+ '#title' => $this->t('What standardized tests did you take?'),
+ '#description' => 'Checkboxes, #type = checkboxes',
+ ];
+
+ // Color.
+ $form['color'] = [
+ '#type' => 'color',
+ '#title' => $this->t('Color'),
+ '#default_value' => '#ffffff',
+ '#description' => 'Color, #type = color',
+ ];
+
+ // Date.
+ $form['expiration'] = [
+ '#type' => 'date',
+ '#title' => $this->t('Content expiration'),
+ '#default_value' => ['year' => 2020, 'month' => 2, 'day' => 15],
+ '#description' => 'Date, #type = date',
+ ];
+
+ // Date-time.
+ $form['datetime'] = [
+ '#type' => 'datetime',
+ '#title' => 'Date Time',
+ '#date_increment' => 1,
+ '#date_timezone' => drupal_get_user_timezone(),
+ '#default_value' => drupal_get_user_timezone(),
+ '#description' => $this->t('Date time, #type = datetime'),
+ ];
+
+ // URL.
+ $form['url'] = [
+ '#type' => 'url',
+ '#title' => $this->t('URL'),
+ '#maxlength' => 255,
+ '#size' => 30,
+ '#description' => $this->t('URL, #type = url'),
+ ];
+
+ // Email.
+ $form['email'] = [
+ '#type' => 'email',
+ '#title' => $this->t('Email'),
+ '#description' => $this->t('Email, #type = email'),
+ ];
+
+ // Number.
+ $form['quantity'] = [
+ '#type' => 'number',
+ '#title' => t('Quantity'),
+ '#description' => $this->t('Number, #type = number'),
+ ];
+
+ // Password.
+ $form['password'] = [
+ '#type' => 'password',
+ '#title' => $this->t('Password'),
+ '#description' => 'Password, #type = password',
+ ];
+
+ // Password Confirm.
+ $form['password_confirm'] = [
+ '#type' => 'password_confirm',
+ '#title' => $this->t('New Password'),
+ '#description' => $this->t('PasswordConfirm, #type = password_confirm'),
+ ];
+
+ // Range.
+ $form['size'] = [
+ '#type' => 'range',
+ '#title' => t('Size'),
+ '#min' => 10,
+ '#max' => 100,
+ '#description' => $this->t('Range, #type = range'),
+ ];
+
+ // Radios.
+ $form['settings']['active'] = [
+ '#type' => 'radios',
+ '#title' => t('Poll status'),
+ '#options' => [0 => $this->t('Closed'), 1 => $this->t('Active')],
+ '#description' => $this->t('Radios, #type = radios'),
+ ];
+
+ // Search.
+ $form['search'] = [
+ '#type' => 'search',
+ '#title' => $this->t('Search'),
+ '#description' => $this->t('Search, #type = search'),
+ ];
+
+ // Select.
+ $form['favorite'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Favorite color'),
+ '#options' => [
+ 'red' => $this->t('Red'),
+ 'blue' => $this->t('Blue'),
+ 'green' => $this->t('Green'),
+ ],
+ '#empty_option' => $this->t('-select-'),
+ '#description' => $this->t('Select, #type = select'),
+ ];
+
+ // Multiple values option elements.
+ $form['select_multiple'] = [
+ '#type' => 'select',
+ '#title' => 'Select (multiple)',
+ '#multiple' => TRUE,
+ '#options' => [
+ 'sat' => 'SAT',
+ 'act' => 'ACT',
+ 'none' => 'N/A',
+ ],
+ '#default_value' => ['sat'],
+ '#description' => 'Select Multiple',
+ ];
+
+ // Tel.
+ $form['phone'] = [
+ '#type' => 'tel',
+ '#title' => $this->t('Phone'),
+ '#description' => $this->t('Tel, #type = tel'),
+ ];
+
+ // Details.
+ $form['details'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Details'),
+ '#description' => $this->t('Details, #type = details'),
+ ];
+
+ // TableSelect.
+ $options = [
+ 1 => ['first_name' => 'Indy', 'last_name' => 'Jones'],
+ 2 => ['first_name' => 'Darth', 'last_name' => 'Vader'],
+ 3 => ['first_name' => 'Super', 'last_name' => 'Man'],
+ ];
+
+ $header = [
+ 'first_name' => t('First Name'),
+ 'last_name' => t('Last Name'),
+ ];
+
+ $form['table'] = [
+ '#type' => 'tableselect',
+ '#title' => $this->t('Users'),
+ '#header' => $header,
+ '#options' => $options,
+ '#empty' => t('No users found'),
+ ];
+
+ // Textarea.
+ $form['text'] = [
+ '#type' => 'textarea',
+ '#title' => $this->t('Text'),
+ '#description' => $this->t('Textarea, #type = textarea'),
+ ];
+
+ // Text format.
+ $form['text_format'] = [
+ '#type' => 'text_format',
+ '#title' => 'Text format',
+ '#format' => 'plain_text',
+ '#expected_value' => [
+ 'value' => 'Text value',
+ 'format' => 'plain_text',
+ ],
+ '#textformat_value' => [
+ 'value' => 'Testvalue',
+ 'format' => 'filtered_html',
+ ],
+ '#description' => $this->t('Text format, #type = text_format'),
+ ];
+
+ // Textfield.
+ $form['subject'] = [
+ '#type' => 'textfield',
+ '#title' => t('Subject'),
+ '#size' => 60,
+ '#maxlength' => 128,
+ '#description' => $this->t('Textfield, #type = textfield'),
+ ];
+
+ // Weight.
+ $form['weight'] = [
+ '#type' => 'weight',
+ '#title' => t('Weight'),
+ '#delta' => 10,
+ '#description' => $this->t('Weight, #type = weight'),
+ ];
+
+ // Group submit handlers in an actions element with a key of "actions" so
+ // that it gets styled correctly, and so that other modules may add actions
+ // to the form.
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+
+ // Extra actions for the display.
+ $form['actions']['extra_actions'] = [
+ '#type' => 'dropbutton',
+ '#links' => [
+ 'simple_form' => [
+ 'title' => $this->t('Simple Form'),
+ 'url' => Url::fromRoute('form_api_example.simple_form'),
+ ],
+ 'demo' => [
+ 'title' => $this->t('Build Demo'),
+ 'url' => Url::fromRoute('form_api_example.build_demo'),
+ ],
+ ],
+ ];
+
+ // File.
+ $form['file'] = [
+ '#type' => 'file',
+ '#title' => 'File',
+ '#description' => $this->t('File, #type = file'),
+ ];
+
+ // Manage file.
+ $form['managed_file'] = [
+ '#type' => 'managed_file',
+ '#title' => 'Managed file',
+ '#description' => $this->t('Manage file, #type = managed_file'),
+ ];
+
+ // Image Buttons.
+ $form['image_button'] = [
+ '#type' => 'image_button',
+ '#value' => 'Image button',
+ '#src' => drupal_get_path('module', 'examples') . '/images/100x30.svg',
+ '#description' => $this->t('image file, #type = image_button'),
+ ];
+
+ // Button.
+ $form['button'] = [
+ '#type' => 'button',
+ '#value' => 'Button',
+ '#description' => $this->t('Button, #type = button'),
+ ];
+
+ // Add a submit button that handles the submission of the form.
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ '#description' => $this->t('Submit, #type = submit'),
+ ];
+
+ // Add a reset button that handles the submission of the form.
+ $form['actions']['reset'] = [
+ '#type' => 'button',
+ '#button_type' => 'reset',
+ '#value' => t('Reset'),
+ '#description' => $this->t('Submit, #type = button, #button_type = reset, #attributes = this.form.reset();return false'),
+ '#attributes' => [
+ 'onclick' => 'this.form.reset(); return false;',
+ ],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'form_api_example_input_demo_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Find out what was submitted.
+ $values = $form_state->getValues();
+ foreach ($values as $key => $value) {
+ $label = isset($form[$key]['#title']) ? $form[$key]['#title'] : $key;
+
+ // Many arrays return 0 for unselected values so lets filter that out.
+ if (is_array($value)) {
+ $value = array_filter($value);
+ }
+
+ // Only display for controls that have titles and values.
+ if ($value && $label) {
+ $display_value = is_array($value) ? preg_replace('/[\n\r\s]+/', ' ', print_r($value, 1)) : $value;
+ $message = $this->t('Value for %title: %value', ['%title' => $label, '%value' => $display_value]);
+ drupal_set_message($message);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/ModalForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/ModalForm.php
new file mode 100644
index 000000000..b6ccaec7e
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/ModalForm.php
@@ -0,0 +1,192 @@
+setRequestStack($container->get('request_stack'));
+ $form->setStringTranslation($container->get('string_translation'));
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'form_api_example_modal_form';
+ }
+
+ /**
+ * Helper method so we can have consistent dialog options.
+ *
+ * @return string[]
+ * An array of jQuery UI elements to pass on to our dialog form.
+ */
+ protected static function getDataDialogOptions() {
+ return [
+ 'width' => '50%',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $nojs = NULL) {
+ // Add the core AJAX library.
+ $form['#attached']['library'][] = 'core/drupal.ajax';
+
+ $form['description'] = [
+ '#type' => 'item',
+ '#markup' => $this->t('This example demonstrates a form that can work as a normal multi-request form, or as a modal dialog using AJAX.'),
+ ];
+ // Add a link to show this form in a modal dialog if we're not already in
+ // one.
+ if ($nojs == 'nojs') {
+ $form['use_ajax_container'] = [
+ '#type' => 'details',
+ '#open' => TRUE,
+ ];
+ $form['use_ajax_container']['description'] = [
+ '#type' => 'item',
+ '#markup' => $this->t('In order to show a modal dialog by clicking on a link, that link has to have class use-ajax and data-dialog-type="modal". This link has those attributes.'),
+ ];
+ $form['use_ajax_container']['use_ajax'] = [
+ '#type' => 'link',
+ '#title' => $this->t('See this form as a modal.'),
+ '#url' => Url::fromRoute('form_api_example.modal_form', ['nojs' => 'ajax']),
+ '#attributes' => [
+ 'class' => ['use-ajax'],
+ 'data-dialog-type' => 'modal',
+ 'data-dialog-options' => json_encode(static::getDataDialogOptions()),
+ // Add this id so that we can test this form.
+ 'id' => 'ajax-example-modal-link',
+ ],
+ ];
+ }
+
+ // This element is responsible for displaying form errors in the AJAX
+ // dialog.
+ if ($nojs == 'ajax') {
+ $form['status_messages'] = [
+ '#type' => 'status_messages',
+ '#weight' => -999,
+ ];
+ }
+
+ $form['title'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title'),
+ '#required' => TRUE,
+ ];
+
+ // Group submit handlers in an actions element with a key of "actions" so
+ // that it gets styled correctly, and so that other modules may add actions
+ // to the form.
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+
+ // Add a submit button that handles the submission of the form.
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ '#ajax' => [
+ 'callback' => '::ajaxSubmitForm',
+ 'event' => 'click',
+ ],
+ ];
+
+ // Set the form to not use AJAX if we're on a nojs path. When this form is
+ // within the modal dialog, Drupal will make sure we're using an AJAX path
+ // instead of a nojs one.
+ if ($nojs == 'nojs') {
+ unset($form['actions']['submit']['#ajax']);
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $title = $form_state->getValue('title');
+ $message = $this->t('Submit handler: You specified a title of @title.', ['@title' => $title]);
+ drupal_set_message($message);
+ }
+
+ /**
+ * Implements the submit handler for the modal dialog AJAX call.
+ *
+ * @param array $form
+ * Render array representing from.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Current form state.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * Array of AJAX commands to execute on submit of the modal form.
+ */
+ public function ajaxSubmitForm(array &$form, FormStateInterface $form_state) {
+ // We begin building a new ajax reponse.
+ $response = new AjaxResponse();
+
+ // If the user submitted the form and there are errors, show them the
+ // input dialog again with error messages. Since the title element is
+ // required, the empty string wont't validate and there will be an error.
+ if ($form_state->getErrors()) {
+ // If there are errors, we can show the form again with the errors in
+ // the status_messages section.
+ $form['status_messages'] = [
+ '#type' => 'status_messages',
+ '#weight' => -10,
+ ];
+ $response->addCommand(new OpenModalDialogCommand($this->t('Errors'), $form, static::getDataDialogOptions()));
+ }
+ // If there are no errors, show the output dialog.
+ else {
+ // We don't want any messages that were added by submitForm().
+ drupal_get_messages();
+ // We use FormattableMarkup to handle sanitizing the input.
+ // @todo: There's probably a better way to do this.
+ $title = new FormattableMarkup(':title', [':title' => $form_state->getValue('title')]);
+ // This will be the contents for the modal dialog.
+ $content = [
+ '#type' => 'item',
+ '#markup' => $this->t('Your specified title of \'%title\' appears in this modal dialog.', ['%title' => $title]),
+ ];
+ // Add the OpenModalDialogCommand to the response. This will cause Drupal
+ // AJAX to show the modal dialog. The user can click the little X to close
+ // the dialog.
+ $response->addCommand(new OpenModalDialogCommand($title, $content, static::getDataDialogOptions()));
+ }
+
+ // Finally return our response.
+ return $response;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/MultistepForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/MultistepForm.php
new file mode 100644
index 000000000..3bfacaf3e
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/MultistepForm.php
@@ -0,0 +1,193 @@
+has('page_num') && $form_state->get('page_num') == 2) {
+ return self::fapiExamplePageTwo($form, $form_state);
+ }
+
+ $form_state->set('page_num', 1);
+
+ $form['description'] = [
+ '#type' => 'item',
+ '#title' => $this->t('A basic multistep form (page 1)'),
+ ];
+
+ $form['first_name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('First Name'),
+ '#description' => $this->t('Enter your first name.'),
+ '#default_value' => $form_state->getValue('first_name', ''),
+ '#required' => TRUE,
+ ];
+
+ $form['last_name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Last Name'),
+ '#default_value' => $form_state->getValue('last_name', ''),
+ '#description' => $this->t('Enter your last name.'),
+ ];
+
+ $form['birth_year'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Birth Year'),
+ '#default_value' => $form_state->getValue('birth_year', ''),
+ '#description' => $this->t('Format is "YYYY" and value between 1900 and 2000'),
+ ];
+
+ // Group submit handlers in an actions element with a key of "actions" so
+ // that it gets styled correctly, and so that other modules may add actions
+ // to the form. This is not required, but is convention.
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+
+ $form['actions']['next'] = [
+ '#type' => 'submit',
+ '#button_type' => 'primary',
+ '#value' => $this->t('Next'),
+ // Custom submission handler for page 1.
+ '#submit' => ['::fapiExampleMultistepFormNextSubmit'],
+ // Custom validation handler for page 1.
+ '#validate' => ['::fapiExampleMultistepFormNextValidate'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $page_values = $form_state->get('page_values');
+
+ drupal_set_message($this->t('The form has been submitted. name="@first @last", year of birth=@year_of_birth', [
+ '@first' => $page_values['first_name'],
+ '@last' => $page_values['last_name'],
+ '@year_of_birth' => $page_values['birth_year'],
+ ]));
+
+ drupal_set_message($this->t('And the favorite color is @color', ['@color' => $form_state->getValue('color')]));
+ }
+
+ /**
+ * Provides custom validation handler for page 1.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function fapiExampleMultistepFormNextValidate(array &$form, FormStateInterface $form_state) {
+ $birth_year = $form_state->getValue('birth_year');
+
+ if ($birth_year != '' && ($birth_year < 1900 || $birth_year > 2000)) {
+ // Set an error for the form element with a key of "birth_year".
+ $form_state->setErrorByName('birth_year', $this->t('Enter a year between 1900 and 2000.'));
+ }
+ }
+
+ /**
+ * Provides custom submission handler for page 1.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function fapiExampleMultistepFormNextSubmit(array &$form, FormStateInterface $form_state) {
+ $form_state
+ ->set('page_values', [
+ // Keep only first step values to minimize stored data.
+ 'first_name' => $form_state->getValue('first_name'),
+ 'last_name' => $form_state->getValue('last_name'),
+ 'birth_year' => $form_state->getValue('birth_year'),
+ ])
+ ->set('page_num', 2)
+ ->setRebuild(TRUE);
+ }
+
+ /**
+ * Builds the second step form (page 2).
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array
+ * The render array defining the elements of the form.
+ */
+ public function fapiExamplePageTwo(array &$form, FormStateInterface $form_state) {
+
+ $form['description'] = [
+ '#type' => 'item',
+ '#title' => $this->t('A basic multistep form (page 2)'),
+ ];
+
+ $form['color'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Favorite color'),
+ '#required' => TRUE,
+ '#default_value' => $form_state->getValue('color', ''),
+ ];
+ $form['back'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Back'),
+ // Custom submission handler for 'Back' button.
+ '#submit' => ['::fapiExamplePageTwoBack'],
+ // We won't bother validating the required 'color' field, since they
+ // have to come back to this page to submit anyway.
+ '#limit_validation_errors' => [],
+ ];
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#button_type' => 'primary',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Provides custom submission handler for 'Back' button (page 2).
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function fapiExamplePageTwoBack(array &$form, FormStateInterface $form_state) {
+ $form_state
+ // Restore values for the first step.
+ ->setValues($form_state->get('page_values'))
+ ->set('page_num', 1)
+ ->setRebuild(TRUE);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/SimpleForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/SimpleForm.php
new file mode 100644
index 000000000..5b105362f
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/SimpleForm.php
@@ -0,0 +1,114 @@
+ 'item',
+ '#markup' => $this->t('This basic example shows a single text input element and a submit button'),
+ ];
+
+ $form['title'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Title'),
+ '#description' => $this->t('Title must be at least 5 characters in length.'),
+ '#required' => TRUE,
+ ];
+
+ // Group submit handlers in an actions element with a key of "actions" so
+ // that it gets styled correctly, and so that other modules may add actions
+ // to the form. This is not required, but is convention.
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+
+ // Add a submit button that handles the submission of the form.
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Getter method for Form ID.
+ *
+ * The form ID is used in implementations of hook_form_alter() to allow other
+ * modules to alter the render array built by this form controller. it must
+ * be unique site wide. It normally starts with the providing module's name.
+ *
+ * @return string
+ * The unique ID of the form defined by this class.
+ */
+ public function getFormId() {
+ return 'form_api_example_simple_form';
+ }
+
+ /**
+ * Implements form validation.
+ *
+ * The validateForm method is the default method called to validate input on
+ * a form.
+ *
+ * @param array $form
+ * The render array of the currently built form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Object describing the current state of the form.
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $title = $form_state->getValue('title');
+ if (strlen($title) < 5) {
+ // Set an error for the form element with a key of "title".
+ $form_state->setErrorByName('title', $this->t('The title must be at least 5 characters long.'));
+ }
+ }
+
+ /**
+ * Implements a form submit handler.
+ *
+ * The submitForm method is the default method called for any submit elements.
+ *
+ * @param array $form
+ * The render array of the currently built form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Object describing the current state of the form.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /*
+ * This would normally be replaced by code that actually does something
+ * with the title.
+ */
+ $title = $form_state->getValue('title');
+ drupal_set_message(t('You specified a title of %title.', ['%title' => $title]));
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/StateDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/StateDemo.php
new file mode 100644
index 000000000..e047f2c85
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/StateDemo.php
@@ -0,0 +1,89 @@
+ 'item',
+ '#markup' => $this->t('This example demonstrates the #states property. #states makes an element visibility dependent on another.'),
+ ];
+
+ $form['needs_accommodation'] = [
+ '#type' => 'checkbox',
+ '#title' => 'Need Special Accommodations?',
+ ];
+
+ // The #states property used here binds the visibility of the of the
+ // container element to the value of the needs_accommodation checkbox above.
+ $form['accommodation'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => 'accommodation',
+ ],
+ '#states' => [
+ 'invisible' => [
+ 'input[name="needs_accommodation"]' => ['checked' => FALSE],
+ ],
+ ],
+ ];
+
+ $form['accommodation']['diet'] = [
+ '#type' => 'textfield',
+ '#title' => t('Dietary Restrictions'),
+ ];
+
+ // Add a submit button that handles the submission of the form.
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Getter method for Form ID.
+ *
+ * @inheritdoc
+ */
+ public function getFormId() {
+ return 'form_api_example_state_demo';
+ }
+
+ /**
+ * Implements submitForm callback.
+ *
+ * @inheritdoc
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Find out what was submitted.
+ $values = $form_state->getValues();
+ if ($values['needs_accommodation']) {
+ drupal_set_message($this->t('Dietary Restriction Requested: %diet', ['%diet' => $values['diet']]));
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/VerticalTabsDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/VerticalTabsDemo.php
new file mode 100644
index 000000000..4a6d35dd3
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/src/Form/VerticalTabsDemo.php
@@ -0,0 +1,76 @@
+ 'item',
+ '#markup' => $this->t('This example demonstrates the use of vertical tabs to group elements.'),
+ ];
+
+ $form['information'] = [
+ '#type' => 'vertical_tabs',
+ '#default_tab' => 'edit-publication',
+ ];
+
+ $form['author'] = [
+ '#type' => 'details',
+ '#title' => 'Author',
+ '#group' => 'information',
+ ];
+
+ $form['author']['name'] = [
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ ];
+
+ $form['publication'] = [
+ '#type' => 'details',
+ '#title' => t('Publication'),
+ '#group' => 'information',
+ ];
+
+ $form['publication']['publisher'] = [
+ '#type' => 'textfield',
+ '#title' => t('Publisher'),
+ ];
+
+ $form['actions'] = ['#type' => 'actions'];
+ // Add a submit button that handles the submission of the form.
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Submit'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Getter method for Form ID.
+ *
+ * @inheritdoc
+ */
+ public function getFormId() {
+ return 'form_api_example_vertical_tabs_demo';
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/templates/description.html.twig b/sites/all/modules/examples/examples/form_api_example/templates/description.html.twig
new file mode 100644
index 000000000..6740778c5
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/templates/description.html.twig
@@ -0,0 +1,31 @@
+{#
+
+Description text for the Fapi Example.
+
+#}
+
+{{ attach_library('core/drupal.dialog.ajax') }}
+{% set simple_form = path('form_api_example.simple_form') %}
+{% set multistep_form = path('form_api_example.multistep_form') %}
+{% set input_demo = path('form_api_example.input_demo') %}
+{% set build_demo = path('form_api_example.build_demo') %}
+{% set container_demo = path('form_api_example.container_demo') %}
+{% set state_demo = path('form_api_example.state_demo') %}
+{% set vertical_tabs_demo = path('form_api_example.vertical_tabs_demo') %}
+{% set ajax_demo = path('form_api_example.ajax_color_demo') %}
+{% set ajax_addmore = path('form_api_example.ajax_addmore') %}
+{% set modal_form = path('form_api_example.modal_form') %}
+{% trans %}
+
+
Form examples to demonstrate common UI solutions using the Drupal Form API
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/FapiExampleTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/FapiExampleTest.php
new file mode 100644
index 000000000..a30f6a7a6
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/FapiExampleTest.php
@@ -0,0 +1,325 @@
+doTestRoutes();
+
+ $this->doTestAjaxAddMore();
+ $this->doTestAjaxColorForm();
+ $this->doTestBuildDemo();
+ $this->doTestContainerDemoForm();
+ $this->doTestInputDemoForm();
+ $this->doTestModalForm();
+ $this->doTestSimpleFormExample();
+ $this->doTestStateDemoForm();
+ $this->doTestVerticalTabsDemoForm();
+ }
+
+ /**
+ * Tests links.
+ */
+ public function doTestRoutes() {
+ $assertion = $this->assertSession();
+
+ // Routes with menu links, and their form buttons.
+ $routes = [
+ 'form_api_example.description' => [],
+ 'form_api_example.simple_form' => ['Submit'],
+ 'form_api_example.input_demo' => ['Submit'],
+ 'form_api_example.state_demo' => ['Submit'],
+ 'form_api_example.container_demo' => ['Submit'],
+ 'form_api_example.vertical_tabs_demo' => ['Submit'],
+ // Modal form has a submit button, but requires input.
+ 'form_api_example.modal_form' => [],
+ 'form_api_example.ajax_color_demo' => ['Submit'],
+ 'form_api_example.build_demo' => ['Submit'],
+ 'form_api_example.ajax_addmore' => ['Submit'],
+ // Multistep form has submit buttons, but requires input.
+ 'form_api_example.multistep_form' => [],
+ ];
+
+ // Ensure the links appear in the tools menu sidebar.
+ $this->drupalGet('');
+ foreach (array_keys($routes) as $route) {
+ $assertion->linkByHrefExists(Url::fromRoute($route)->getInternalPath());
+ }
+
+ // Go to all the routes and click all the buttons.
+ foreach ($routes as $route => $buttons) {
+ $path = Url::fromRoute($route);
+ error_log($route);
+ $this->drupalGet($path);
+ $assertion->statusCodeEquals(200);
+ foreach ($buttons as $button) {
+ $this->drupalPostForm($path, [], $button);
+ $assertion->statusCodeEquals(200);
+ }
+ }
+ }
+
+ /**
+ * Test the ajax demo form.
+ */
+ public function doTestAjaxColorForm() {
+ $assert = $this->assertSession();
+
+ // Post the form.
+ $edit = [
+ 'temperature' => 'warm',
+ ];
+ $this->drupalPostForm(Url::fromRoute('form_api_example.ajax_color_demo'), $edit, 'Submit');
+ $assert->statusCodeEquals(200);
+ $assert->pageTextContains('Value for Temperature: warm');
+ }
+
+ /**
+ * Test the build demo form.
+ */
+ public function doTestBuildDemo() {
+ $assert = $this->assertSession();
+ $build_demo_url = Url::fromRoute('form_api_example.build_demo');
+
+ $edit = [
+ 'change' => '1',
+ ];
+ $this->drupalPostForm($build_demo_url, $edit, 'Submit');
+
+ $assert->pageTextContains('1. __construct');
+ $assert->pageTextContains('2. getFormId');
+ $assert->pageTextContains('3. validateForm');
+ $assert->pageTextContains('4. submitForm');
+
+ // Ensure the 'submit rebuild' action performs the rebuild.
+ $this->drupalPostForm($build_demo_url, $edit, 'Submit Rebuild');
+ $assert->pageTextContains('4. rebuildFormSubmit');
+ }
+
+ /**
+ * Test the container demo form.
+ */
+ public function doTestContainerDemoForm() {
+ $assert = $this->assertSession();
+
+ // Post the form.
+ $edit = [
+ 'name' => 'Dave',
+ 'pen_name' => 'DMan',
+ 'title' => 'My Book',
+ 'publisher' => 'me',
+ 'diet' => 'vegan',
+ ];
+ $this->drupalPostForm(Url::fromRoute('form_api_example.container_demo'), $edit, 'Submit');
+ $assert->pageTextContains('Value for name: Dave');
+ $assert->pageTextContains('Value for pen_name: DMan');
+ $assert->pageTextContains('Value for title: My Book');
+ $assert->pageTextContains('Value for publisher: me');
+ $assert->pageTextContains('Value for diet: vegan');
+ }
+
+ /**
+ * Test the input demo form.
+ */
+ public function doTestInputDemoForm() {
+ $assert = $this->assertSession();
+
+ // Post the form.
+ $edit = [
+ 'tests_taken[SAT]' => TRUE,
+ 'color' => '#2b49ff',
+ 'expiration' => '2015-10-21',
+ 'datetime[date]' => '2017-12-07 15:32:10',
+ 'url' => 'https://www.drupal.org',
+ 'email' => 'somebody@example.org',
+ 'quantity' => '4',
+ 'password' => 'letmein',
+ 'password_confirm[pass1]' => 'letmein',
+ 'password_confirm[pass2]' => 'letmein',
+ 'size' => '76',
+ 'active' => '1',
+ 'search' => 'my search string',
+ 'favorite' => 'blue',
+ 'select_multiple[]' => ['act'],
+ 'phone' => '555-555-5555',
+ 'table[1]' => TRUE,
+ 'table[3]' => TRUE,
+ 'text' => 'This is a test of my form.',
+ 'text_format[value]' => 'Examples for Developers',
+ 'subject' => 'Form test',
+ 'weight' => '3',
+ ];
+ $this->drupalPostForm(Url::fromRoute('form_api_example.input_demo'), $edit, 'Submit');
+ $assert->statusCodeEquals(200);
+
+ $assert->pageTextContains('Value for What standardized tests did you take?');
+ $assert->pageTextContains('Value for Color: #2b49ff');
+ $assert->pageTextContains('Value for Content expiration: 2015-10-21');
+ $assert->pageTextContains('Value for Date Time: 2017-12-07 15:32:10');
+ $assert->pageTextContains('Value for URL: https://www.drupal.org');
+ $assert->pageTextContains('Value for Email: somebody@example.org');
+ $assert->pageTextContains('Value for Quantity: 4');
+ $assert->pageTextContains('Value for Password: letmein');
+ $assert->pageTextContains('Value for New Password: letmein');
+ $assert->pageTextContains('Value for Size: 76');
+ $assert->pageTextContains('Value for active: 1');
+ $assert->pageTextContains('Value for Search: my search string');
+ $assert->pageTextContains('Value for Favorite color: blue');
+ $assert->pageTextContains('Value for Select (multiple): Array ( [act] => act )');
+ $assert->pageTextContains('Value for Phone: 555-555-5555');
+ $assert->pageTextContains('Value for Users: Array ( [1] => 1 [3] => 3 )');
+ $assert->pageTextContains('Value for Text: This is a test of my form.');
+ $assert->pageTextContains('Value for Text format: Array ( [value] => Examples for Developers [format] => plain_text )');
+ $assert->pageTextContains('Value for Subject: Form test');
+ $assert->pageTextContains('Value for Weight: 3');
+ }
+
+ /**
+ * Test the modal form.
+ */
+ public function doTestModalForm() {
+ $assert = $this->assertSession();
+
+ // Post the form.
+ $edit = [
+ 'title' => 'My Book',
+ ];
+ $this->drupalPostForm(Url::fromRoute('form_api_example.modal_form'), $edit, 'Submit');
+ $assert->pageTextContains('Submit handler: You specified a title of My Book.');
+ }
+
+ /**
+ * Check routes defined by form_api_example.
+ */
+ public function doTestSimpleFormExample() {
+ $assert = $this->assertSession();
+
+ // Post a title.
+ $edit = ['title' => 'My Custom Title'];
+ $this->drupalPostForm(Url::fromRoute('form_api_example.simple_form'), $edit, 'Submit');
+ $assert->pageTextContains('You specified a title of My Custom Title.');
+ }
+
+ /**
+ * Test the state demo form.
+ */
+ public function doTestStateDemoForm() {
+ $assert = $this->assertSession();
+
+ // Post the form.
+ $edit = [
+ 'needs_accommodation' => TRUE,
+ 'diet' => 'vegan',
+ ];
+ $this->drupalPostForm(Url::fromRoute('form_api_example.state_demo'), $edit, 'Submit');
+ $assert->pageTextContains('Dietary Restriction Requested: vegan');
+ }
+
+ /**
+ * Test the vertical tabs demo form.
+ */
+ public function doTestVerticalTabsDemoForm() {
+ $assert = $this->assertSession();
+
+ // Post the form.
+ $edit = [
+ 'name' => 'Dave',
+ 'publisher' => 'me',
+ ];
+ $this->drupalPostForm(Url::fromRoute('form_api_example.container_demo'), $edit, 'Submit');
+ $assert->pageTextContains('Value for name: Dave');
+ $assert->pageTextContains('Value for publisher: me');
+ }
+
+ /**
+ * Test the Ajax Add More demo form.
+ */
+ public function doTestAjaxAddMore() {
+ // XPath for the remove button. We have to use contains() here because the
+ // ID will have a hash value at the end.
+ $button_xpath = '//input[contains(@id,"edit-names-fieldset-actions-remove-name")]';
+
+ $ajax_addmore_url = Url::fromRoute('form_api_example.ajax_addmore');
+
+ // Verify that anonymous can access the ajax_add_more page.
+ $this->drupalGet($ajax_addmore_url);
+ $this->assertResponse(200, 'The Demo of Container page is available.');
+ // Verify that there is no remove button.
+ $this->assertFalse($this->xpath($button_xpath));
+
+ $name_one = 'John';
+ $name_two = 'Smith';
+
+ // Enter the value in field-1.
+ // and click on 'Add one more' button.
+ $edit = [];
+ $edit['names_fieldset[name][0]'] = $name_one;
+ $this->drupalPostForm($ajax_addmore_url, $edit, 'Add one more');
+
+ // Verify field-2 gets added.
+ // and value of field-1 should retained.
+ $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-0"]'), $name_one);
+ $this->assertTrue($this->xpath('//input[@id = "edit-names-fieldset-name-1"]'));
+ // Verify that the remove button was added.
+ $this->assertTrue($this->xpath($button_xpath));
+
+ // Enter the value in field-2
+ // and click on 'Add one more' button.
+ $edit['names_fieldset[name][1]'] = $name_two;
+ $this->drupalPostForm(NULL, $edit, 'Add one more');
+
+ // Verify field-3 gets added.
+ // and value of field-1 and field-2 are retained.
+ $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-0"]'), $name_one);
+ $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-1"]'), $name_two);
+ $this->assertTrue($this->xpath('//input[@id = "edit-names-fieldset-name-2"]'));
+
+ // Click on "Remove one" button to test remove button works.
+ // and value of field-1 and field-2 are retained.
+ $this->drupalPostForm(NULL, NULL, 'Remove one');
+ $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-0"]'), $name_one);
+ $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-1"]'), $name_two);
+ $this->assertFalse($this->xpath('//input[@id = "edit-names-fieldset-name-2"]'));
+
+ // Submit the form and verify the results.
+ $this->drupalPostForm(NULL, NULL, 'Submit');
+ $this->assertText('These people are coming to the picnic: ' . $name_one . ', ' . $name_two);
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/MultistepFormTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/MultistepFormTest.php
new file mode 100644
index 000000000..95c1226e7
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/MultistepFormTest.php
@@ -0,0 +1,68 @@
+drupalGet(Url::fromRoute('form_api_example.multistep_form'));
+ $page = $this->getSession()->getPage();
+ $h1 = $page->find('css', 'h1');
+ $this->assertContains('Multistep form', $h1->getText());
+ $desc = $page->find('css', '#edit-description label');
+ $this->assertContains('page 1', $desc->getText());
+ $this->submitForm([
+ 'first_name' => 'Bozo',
+ 'last_name' => 'Di Clown',
+ 'birth_year' => 1980,
+ ],
+ 'Next');
+
+ // Really new page?
+ $page2 = $this->getSession()->getPage();
+ $desc = $page2->find('css', '#edit-description label');
+ $this->assertContains('page 2', $desc->getText());
+
+ // Try the back button.
+ $this->submitForm([], 'Back');
+ $page1 = $this->getSession()->getPage();
+ $desc = $page1->find('css', '#edit-description label');
+ $this->assertContains('page 1', $desc->getText());
+ // Is the form still filled out?
+ $first_name = $page1->findField('first_name')->getValue();
+ $this->assertEquals('Bozo', $first_name);
+ $second_name = $page1->findField('last_name')->getValue();
+ $this->assertEquals('Di Clown', $second_name);
+ $birth_year = $page1->findField('birth_year')->getValue();
+ $this->assertEquals('1980', $birth_year);
+
+ // Back to the second page.
+ $this->click('#edit-next');
+ $page2 = $this->getSession()->getPage();
+ $desc = $page2->find('css', '#edit-description label');
+ $this->assertContains('page 2', $desc->getText());
+ $this->submitForm(['color' => 'neon green'], 'Submit');
+
+ // This should take us back to the first page with a status message.
+ $messages = $this->getSession()->getPage()->find('css', 'ul.messages__list');
+ $message_text = $messages->getHtml();
+ $this->assertContains('Bozo Di Clown', $message_text);
+ $this->assertContains('1980', $message_text);
+ $this->assertContains('neon green', $message_text);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/AjaxColorFormTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/AjaxColorFormTest.php
new file mode 100644
index 000000000..afe157369
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/AjaxColorFormTest.php
@@ -0,0 +1,83 @@
+drupalGet(Url::fromRoute('form_api_example.ajax_color_demo'));
+
+ // Get Mink stuff.
+ $assert = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Before the color temperature dropdown is selected, we should not have a
+ // color dropdown.
+ $this->assertEmpty($page->find('css', 'select[name="color"]'));
+
+ $color_matrix = [
+ 'warm' => ['red', 'orange', 'yellow'],
+ 'cool' => ['blue', 'purple', 'green'],
+ ];
+
+ foreach ($color_matrix as $temperature => $colors) {
+ // Enter a color temperature.
+ $this->assertNotEmpty(
+ $color_temperature = $page->find('css', 'select[name="temperature"]')
+ );
+ $color_temperature->setValue($temperature);
+ $assert->assertWaitOnAjaxRequest();
+
+ // Find the color dropdown.
+ $this->assertNotEmpty(
+ $color_select = $page->find('css', 'select[name="color"]')
+ );
+
+ // Make sure all the correct color options are present.
+ $this->assertNotEmpty(
+ $color_options = $color_select->findAll('css', 'option')
+ );
+ $this->assertCount(count($colors), $color_options);
+ foreach ($color_options as $color_element) {
+ $this->assertContains($color_element->getValue(), $colors);
+ }
+
+ // Submit all the colors.
+ foreach ($colors as $color) {
+ $page->find('css', 'select[name="temperature"]')->setValue($temperature);
+ $assert->assertWaitOnAjaxRequest();
+ $page->find('css', 'select[name="color"]')->setValue($color);
+ $page->findButton('Submit')->click();
+ $assert->pageTextContains("Value for Temperature: $temperature");
+ $assert->pageTextContains("Value for color: $color");
+ }
+ }
+
+ // Finally, we can make sure that when we 'unset' the temperature dropdown,
+ // the color dropdown goes away.
+ $this->getSession()->getPage()->find('css', 'select[name="temperature"]')
+ ->setValue('');
+ $assert->assertWaitOnAjaxRequest();
+ $this->assertEmpty($page->find('css', 'select[name="color"]'));
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/ModalFormTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/ModalFormTest.php
new file mode 100644
index 000000000..3322bcf14
--- /dev/null
+++ b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/ModalFormTest.php
@@ -0,0 +1,75 @@
+ 'nojs']);
+ $this->drupalGet($modal_route_nojs);
+
+ // Get Mink stuff.
+ $assert = $this->assertSession();
+ $session = $this->getSession();
+ $page = $this->getSession()->getPage();
+
+ // Click on 'see this form as a modal'.
+ $this->clickLink('ajax-example-modal-link');
+
+ $this->assertNotEmpty($assert->waitForElementVisible('css', '.ui-dialog'));
+
+ // Enter a value.
+ $this->assertNotEmpty($input = $page->find('css', 'div.ui-dialog input[name="title"]'));
+ $input->setValue('test_title');
+
+ // Click 'submit'.
+ // @todo: Switch to using NodeElement::click() on the button or
+ // NodeElement::submit() on the form when #2831506 is fixed.
+ // @see https://www.drupal.org/node/2831506
+ $session->executeScript("jQuery('button.ui-button.form-submit').click()");
+ $assert->assertWaitOnAjaxRequest();
+
+ // Check that we have a new modal.
+ $assert->elementContains('css', 'span.ui-dialog-title', 'test_title');
+
+ // Click the close X.
+ // @todo: Switch to using NodeElement::click() on the button or
+ // NodeElement::submit() on the form when #2831506 is fixed.
+ // @see https://www.drupal.org/node/2831506
+ $session->executeScript("jQuery('button.ui-dialog-titlebar-close').click()");
+ $assert->assertWaitOnAjaxRequest();
+
+ // Verify that the modal went away.
+ $assert->pageTextNotContains('appears in this modal dialog.');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.api.php b/sites/all/modules/examples/examples/hooks_example/hooks_example.api.php
new file mode 100644
index 000000000..c3a1a2d6b
--- /dev/null
+++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.api.php
@@ -0,0 +1,53 @@
+ $node->label()]));
+ }
+}
+
+/**
+ * @} End of "addtogroup hooks_example".
+ */
diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.info.yml b/sites/all/modules/examples/examples/hooks_example/hooks_example.info.yml
new file mode 100644
index 000000000..b27b410ea
--- /dev/null
+++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.info.yml
@@ -0,0 +1,14 @@
+name: Hooks Example
+type: module
+description: Provides examples of implementing, defining, and invoking hooks.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:help
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.module b/sites/all/modules/examples/examples/hooks_example/hooks_example.module
new file mode 100644
index 000000000..8a5f2f13a
--- /dev/null
+++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.module
@@ -0,0 +1,226 @@
+' . t('This text is provided by the function hooks_example_help(), which is an implementation of the hook hook_help(). To learn more about how this works checkout the code in hooks_example.module.') . '';
+ }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_view().
+ *
+ * Some hook names include additional tokens that need to be replaced when
+ * implementing the hook. These hooks are dynamic in that when they are being
+ * invoked a portion of their name is replaced with a dynamic value. This is
+ * indicated by placing the token words in all caps. This pattern is often used
+ * in situations where you want to allow modules to generically act on all
+ * instances of a thing, or to act on only a specific subset.
+ *
+ * There are lots of different entity types in Drupal. Node, user, file, etc.
+ * Using hook_entity_view() a module can act on a any entity that is being
+ * viewed, regardless of type. If we wanted to count views of all entities,
+ * regardless of type this would be a good choice. This variant is also useful
+ * if you want to provide administrators with a form where they can choose from
+ * a list of entity types which ones they want to count views for. The logic in
+ * the generic hook implementation could then take that into account and act on
+ * only a select set of entity types.
+ *
+ * If however, you know you only ever want to act on viewing of a node entity
+ * you can instead implement hook_ENTITY_TYPE_view(). Where ENTITY_TYPE is a
+ * token that can be replaced with any valid entity type name.
+ *
+ * @see hook_entity_view()
+ * @see hook_ENTITY_TYPE_view()
+ */
+function hooks_example_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
+ // This example hook implementation keeps track of the number of times a user
+ // has viewed a specific node during their current session. Then displays that
+ // information for them when they view a node.
+ //
+ // In addition, a hook is invoked that allows other modules to react when the
+ // page view count is updated.
+ //
+ // Retrieve the active session from the current request object.
+ $session = \Drupal::request()->getSession();
+ $current_counts = $session->get('hooks_example.view_counts', []);
+ if (!isset($current_counts[$entity->id()])) {
+ // If this is the first time they've viewed the page we need to start the
+ // counter.
+ $current_counts[$entity->id()] = 1;
+ }
+ else {
+ // If they have already viewed this page just increment the existing
+ // counter.
+ $current_counts[$entity->id()]++;
+ }
+
+ // Save the updated values.
+ $session->set('hooks_example.view_counts', $current_counts);
+
+ // Invoke a hook to alert other modules that the count was updated.
+ //
+ // Hooks are invoked via the `module_handler` service. Which is an instance of
+ // \Drupal\Core\Extension\ModuleHandlerInterface.
+ //
+ // Hooks can be invoked in a few different ways:
+ // - All at once using ModuleHandlerInterface::invokeAll() to call all
+ // implementations of the specified hook provided by any enabled module.
+ // - One at a time using ModuleHandlerInterface::invoke() to call only the
+ // the specified module's implementation of a hook.
+ // - Using ModuleHandlerInterface::alter() to pass alterable variables to
+ // hook_TYPE_alter() implementations for all enabled modules. This method
+ // should be used for instances where the calling module has assembled data
+ // and would like to give other modules an opportunity to alter that data
+ // before it's used. A common pattern is to use invokeAll() to first gather
+ // input from other modules, the immediately afterwards call alter() to give
+ // modules the opportunity to alter the aggregate data.
+ $module_handler = \Drupal::moduleHandler();
+
+ // Calling \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll() will
+ // call implementations of the hook in question for all enabled modules. The
+ // method takes two arguments. The name of the hook to invoke, and an optional
+ // array of arguments to pass to any functions implementing the hook.
+ //
+ // Hook names need to be unique. So when defining a new hook in your module it
+ // is customary to prefix the hook name with the short name of your module
+ // followed by the descriptive name of the hook itself. Because hooks names
+ // are also PHP function names they should contain only lowercase alphanumeric
+ // characters and underscores.
+ //
+ // The hook name parameter should have the "hook_" prefix removed. So if you
+ // want to invoke hook_mymodule_do_something() the value used here would be
+ // 'mymodule_do_something'.
+ //
+ // Hook implementations can optionally return a value, depending on the hook
+ // definition. If they do, the invokeAll() method aggregates the responses
+ // from all hooks in an array and returns the array.
+ //
+ // In this example we're invoking hook_hooks_example_count_incremented() and
+ // passing all implementations the current view count for the node, and the
+ // node object itself.
+ $module_handler->invokeAll('hooks_example_count_incremented', [$current_counts[$entity->id()], $entity]);
+
+ // Display the current number of pages the user has viewed along with the
+ // node's content.
+ $build['view_count'] = [
+ '#markup' => '
' . t('You have viewed this node @total times this session.', ['@total' => $current_counts[$entity->id()]]) . '
',
+ // In order for this example to work we disable caching for the content of
+ // this node completely. This ensures that our hook is called every time the
+ // node is viewed instead of using a cached version of the page for
+ // subsequent requests.
+ '#cache' => [
+ 'max-age' => 0,
+ ],
+ ];
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function hooks_example_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ // This is an example of what is known as an alter hook. The $form paremater
+ // in this case represents an already complete Form API array and our hook
+ // implementation is being given the opportunity to make changes to the
+ // existing data structure before it's used. Inovking and alter hooks is a
+ // common pattern anytime lists or complex data structures are assembled.
+ // hook_form_alter(), which allows you to manipulate any form, is one of the
+ // most commonly implemented hooks.
+ //
+ // @see hook_form_alter()
+ // @see hook_form_FORM_ID_alter()
+ //
+ // If this is the user login form, change the description text of the username
+ // field.
+ if ($form_id === 'user_login_form') {
+ $form['name']['#description'] = t('This text has been altered by hooks_example_form_alter().');
+ }
+}
+
+/**
+ * Implements hook_hooks_example_count_incremented().
+ *
+ * Hooks can be implemented by both the module that invokes them like we are
+ * doing here, as well as by any other enabled module.
+ */
+function hooks_example_hooks_example_count_incremented($current_count, NodeInterface $node) {
+ if ($current_count === 1) {
+ drupal_set_message(t('This is the first time you have viewed the node %title.', ['%title' => $node->label()]));
+ }
+}
+
+/**
+ * @} End of "defgroup hooks_example".
+ */
diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.routing.yml b/sites/all/modules/examples/examples/hooks_example/hooks_example.routing.yml
new file mode 100644
index 000000000..76bd59cb5
--- /dev/null
+++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.routing.yml
@@ -0,0 +1,8 @@
+# hooks_example only has one route. It is to a page explaining the module.
+hooks_example.description:
+ path: 'examples/hooks-example'
+ defaults:
+ _controller: '\Drupal\hooks_example\Controller\HooksExampleController::description'
+ _title: 'Hooks Example'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/hooks_example/src/Controller/HooksExampleController.php b/sites/all/modules/examples/examples/hooks_example/src/Controller/HooksExampleController.php
new file mode 100644
index 000000000..548422a25
--- /dev/null
+++ b/sites/all/modules/examples/examples/hooks_example/src/Controller/HooksExampleController.php
@@ -0,0 +1,23 @@
+Implementing, defining, and invoking hooks
+
+
The code in this module implements a couple of new features which you can see
+in action.
+
+
Implements hook_help(): In hooks_example.module you'll find
+an implementation of the hook hook_help() which is used to add contents to this
+module's help overview. Visible at admin/help/hooks_example.
+
+
View counts: hooks_example_node_view() is an implementation
+of the hook hook_ENTITY_TYPE_view() that adds a basic page view counter. You can
+see this in action by navigating to any node on the site and looking for the the
+text telling you how many times you've viewed that page.
+
+
Implements hook_form_alter(): In hookse_example.module
+you'll find an implementation of hook_form_alter() which demonstrates the use of
+one of the most commonly used alter hooks. You can view the altered form at
+user/login.
+
+
To learn more about how to implement an existing hook, or how to define and
+invoke a new hook start by reading the @docblock comments in
+hooks_example.module
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/hooks_example/tests/src/Functional/HooksExampleTest.php b/sites/all/modules/examples/examples/hooks_example/tests/src/Functional/HooksExampleTest.php
new file mode 100644
index 000000000..7158c7093
--- /dev/null
+++ b/sites/all/modules/examples/examples/hooks_example/tests/src/Functional/HooksExampleTest.php
@@ -0,0 +1,84 @@
+createContentType(['type' => 'page']);
+
+ $account = $this->drupalCreateUser(['access administration pages']);
+ $this->drupalLogin($account);
+ }
+
+ /**
+ * Test the output of the example page.
+ */
+ public function testHooksExample() {
+ // Test the description page at examples/hook-example returns a 200.
+ $this->drupalGet('examples/hooks-example');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Test that our implementation of hook_help() works.
+ $this->drupalGet('admin/help/hooks_example');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('This text is provided by the function hooks_example_help()');
+
+ // Test that our implementation of hook_node_view() works.
+ // Create a new node.
+ $settings = [
+ 'type' => 'page',
+ 'title' => 'Hooks Example Testing Node',
+ 'status' => 1,
+ ];
+ $node = $this->drupalCreateNode($settings);
+
+ $this->drupalGet($node->toUrl());
+
+ // Test that the output added to the page by hooks_example_node_view() is
+ // present. Which also tests that our page view counting was initialized.
+ $this->assertSession()->pageTextContains('You have viewed this node 1 times this session.');
+ // Tests that the message set by
+ // hooks_example_hooks_example_count_incremented() is displayed on the page.
+ // Which also has the effect of testing to see wehther or not our custom
+ // hook is being invoked.
+ $this->assertSession()->pageTextContains('This is the first time you have viewed the node ' . $node->label() . '.');
+
+ // Navigate to a new page, and then back and verify the counter was updated.
+ $this->drupalGet('');
+ $this->drupalGet($node->toUrl());
+ $this->assertSession()->pageTextContains('You have viewed this node 2 times this session.');
+ $this->assertSession()->pageTextNotContains('This is the first time you have viewed the node ' . $node->label() . '.');
+
+ // Test our implementation of hook_form_alter().
+ $this->drupalLogout();
+ $this->drupalGet('user/login');
+ $this->assertSession()->pageTextContains('This text has been altered by hooks_example_form_alter().');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/images/100x30.svg b/sites/all/modules/examples/examples/images/100x30.svg
new file mode 100644
index 000000000..3c1b33e17
--- /dev/null
+++ b/sites/all/modules/examples/examples/images/100x30.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/sites/all/modules/examples/examples/images/examples.svg b/sites/all/modules/examples/examples/images/examples.svg
new file mode 100644
index 000000000..49e778305
--- /dev/null
+++ b/sites/all/modules/examples/examples/images/examples.svg
@@ -0,0 +1,56 @@
+
+
+
+
\ No newline at end of file
diff --git a/sites/all/modules/examples/examples/js_example/css/jsweights.css b/sites/all/modules/examples/examples/js_example/css/jsweights.css
new file mode 100755
index 000000000..b132f400c
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/css/jsweights.css
@@ -0,0 +1,4 @@
+.js-weights div {
+ font-size: 20px;
+ font-weight: bold;
+}
diff --git a/sites/all/modules/examples/examples/js_example/js/black.js b/sites/all/modules/examples/examples/js_example/js/black.js
new file mode 100755
index 000000000..038ba8bb0
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js/black.js
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * Contains the definition of the behaviour jsTestBlackWeight.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+ 'use strict';
+
+ /**
+ * Attaches the JS test behavior to to weight div.
+ */
+ Drupal.behaviors.jsTestBlackWeight = {
+ attach: function (context, settings) {
+ var weight = drupalSettings.js_example.js_weights.black;
+ var newDiv = $('').css('color', 'black').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery, Drupal, drupalSettings);
diff --git a/sites/all/modules/examples/examples/js_example/js/blue.js b/sites/all/modules/examples/examples/js_example/js/blue.js
new file mode 100755
index 000000000..88a2497ac
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js/blue.js
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * Contains the definition of the behaviour jsTestBlueWeight.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+ 'use strict';
+
+ /**
+ * Attaches the JS test behavior to weight div.
+ */
+ Drupal.behaviors.jsTestBlueWeight = {
+ attach: function (context, settings) {
+ var weight = drupalSettings.js_example.js_weights.blue;
+ var newDiv = $('').css('color', 'blue').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery, Drupal, drupalSettings);
diff --git a/sites/all/modules/examples/examples/js_example/js/brown.js b/sites/all/modules/examples/examples/js_example/js/brown.js
new file mode 100755
index 000000000..9c25b3a7f
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js/brown.js
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * Contains the definition of the behaviour jsTestBrownWeight.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+ 'use strict';
+
+ /**
+ * Attaches the JS test behavior to weight div.
+ */
+ Drupal.behaviors.jsTestBrownWeight = {
+ attach: function (context, settings) {
+ var weight = drupalSettings.js_example.js_weights.brown;
+ var newDiv = $('').css('color', 'brown').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery, Drupal, drupalSettings);
diff --git a/sites/all/modules/examples/examples/js_example/js/green.js b/sites/all/modules/examples/examples/js_example/js/green.js
new file mode 100755
index 000000000..2c88df4a9
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js/green.js
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * Contains the definition of the behaviour jsTestGreenWeight.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+ 'use strict';
+
+ /**
+ * Attaches the JS test behavior to weight div.
+ */
+ Drupal.behaviors.jsTestGreenWeight = {
+ attach: function (context, settings) {
+ var weight = drupalSettings.js_example.js_weights.green;
+ var newDiv = $('').css('color', 'green').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery, Drupal, drupalSettings);
diff --git a/sites/all/modules/examples/examples/js_example/js/js_example_accordion.js b/sites/all/modules/examples/examples/js_example/js/js_example_accordion.js
new file mode 100644
index 000000000..4cf19df91
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js/js_example_accordion.js
@@ -0,0 +1,13 @@
+/**
+ * @file
+ * Contains js for the accordion example.
+ */
+
+(function ($) {
+
+ 'use strict';
+
+ $(function () {
+ $('#accordion').accordion();
+ });
+})(jQuery);
diff --git a/sites/all/modules/examples/examples/js_example/js/purple.js b/sites/all/modules/examples/examples/js_example/js/purple.js
new file mode 100755
index 000000000..5652ad341
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js/purple.js
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * Contains the definition of the behaviour jsTestPurpleWeight.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+ 'use strict';
+
+ /**
+ * Attaches the JS test behavior to weight div.
+ */
+ Drupal.behaviors.jsTestPurpleWeight = {
+ attach: function (context, settings) {
+ var weight = drupalSettings.js_example.js_weights.purple;
+ var newDiv = $('').css('color', 'purple').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery, Drupal, drupalSettings);
diff --git a/sites/all/modules/examples/examples/js_example/js/red.js b/sites/all/modules/examples/examples/js_example/js/red.js
new file mode 100755
index 000000000..5dfc4274d
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js/red.js
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * Contains the definition of the behaviour jsTestRedWeight.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+ 'use strict';
+
+ /**
+ * Attaches the JS test behavior to weight div.
+ */
+ Drupal.behaviors.jsTestRedWeight = {
+ attach: function (context, settings) {
+ var weight = drupalSettings.js_example.js_weights.red;
+ var newDiv = $('').css('color', 'red').html('I have a weight of ' + weight);
+ $('#js-weights').append(newDiv);
+ }
+ };
+})(jQuery, Drupal, drupalSettings);
diff --git a/sites/all/modules/examples/examples/js_example/js_example.info.yml b/sites/all/modules/examples/examples/js_example/js_example.info.yml
new file mode 100644
index 000000000..4ca2d5e4c
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js_example.info.yml
@@ -0,0 +1,14 @@
+name: JavaScript Example
+description: Demonstrates JavaScript features in Drupal 8.
+type: module
+package: 'Example modules'
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/js_example/js_example.libraries.yml b/sites/all/modules/examples/examples/js_example/js_example.libraries.yml
new file mode 100644
index 000000000..496f74808
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js_example.libraries.yml
@@ -0,0 +1,28 @@
+# A simple script using jquery ui accordion.
+js_example.accordion:
+ js:
+ js/js_example_accordion.js: {}
+ # Build up our dependencies for this page as a library. Our accordion script
+ # needs jquery.ui.accordion. You can find the core scripts under core/assets/.
+ dependencies:
+ - core/jquery.ui.accordion
+
+# Show how weights work.
+js_example.weights:
+ # Add css file.
+ css:
+ component:
+ css/jsweights.css: {}
+ # Add js files.
+ js:
+ # weight is used as example attribute, but please avoid it and instead use
+ # dependencies for order loading.
+ js/red.js: { weight: -4 }
+ js/blue.js: { weight: -2 }
+ js/green.js: { weight: -1 }
+ js/brown.js: { weight: -2 }
+ js/black.js: { weight: -1 }
+ js/purple.js: { weight: -5 }
+ dependencies:
+ - core/jquery
+ - core/drupalSettings
diff --git a/sites/all/modules/examples/examples/js_example/js_example.links.menu.yml b/sites/all/modules/examples/examples/js_example/js_example.links.menu.yml
new file mode 100644
index 000000000..d2e7b0390
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js_example.links.menu.yml
@@ -0,0 +1,18 @@
+# Define default links for this module.
+js_example.info:
+ title: JavaScript Example
+ description: Some information about the JavaScript example.
+ route_name: js_example.info
+ expanded: TRUE
+
+js_example.weights:
+ title: JavaScript weighting in action
+ description: Show list weighting through JavaScript.
+ route_name: js_example.weights
+ parent: js_example.info
+
+js_example.accordion:
+ title: jQuery UI accordion
+ description: jQuery UI accordion demo.
+ route_name: js_example.accordion
+ parent: js_example.info
diff --git a/sites/all/modules/examples/examples/js_example/js_example.module b/sites/all/modules/examples/examples/js_example/js_example.module
new file mode 100755
index 000000000..e35b3680f
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js_example.module
@@ -0,0 +1,37 @@
+ [
+ 'template' => 'accordion',
+ 'variables' => ['title' => NULL],
+ ],
+ ];
+}
+
+/**
+ * @} End of "defgroup js_example".
+ */
diff --git a/sites/all/modules/examples/examples/js_example/js_example.routing.yml b/sites/all/modules/examples/examples/js_example/js_example.routing.yml
new file mode 100755
index 000000000..c25ecf3a6
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/js_example.routing.yml
@@ -0,0 +1,23 @@
+js_example.info:
+ path: '/examples/js-example'
+ defaults:
+ _title: 'JavaScript Example Information'
+ _controller: '\Drupal\js_example\Controller\JsExampleController::description'
+ requirements:
+ _permission: 'access content'
+
+js_example.weights:
+ path: '/examples/js-example/weights'
+ defaults:
+ _title: 'JS Example: see weighting in action'
+ _controller: '\Drupal\js_example\Controller\JsExampleController::getJsWeightImplementation'
+ requirements:
+ _permission: 'access content'
+
+js_example.accordion:
+ path: '/examples/js-example/accordion'
+ defaults:
+ _title: 'JS Example: jQuery UI accordion'
+ _controller: '\Drupal\js_example\Controller\JsExampleController::getJsAccordionImplementation'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/js_example/src/Controller/JsExampleController.php b/sites/all/modules/examples/examples/js_example/src/Controller/JsExampleController.php
new file mode 100755
index 000000000..ccdafcba8
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/src/Controller/JsExampleController.php
@@ -0,0 +1,111 @@
+weight array to drupalSettings, which is where Drupal
+ * passes data out to JavaScript.
+ *
+ * Each of the color scripts (red.js, blue.js, etc) uses jQuery to find our
+ * DIV, and then add some content to it. The order in which the color scripts
+ * execute will end up being the order of the content.
+ *
+ * The 'weight' atttribute in libraries yml file determines the order in which
+ * a script is output to the page. To see this in action:
+ * - Uncheck the 'Aggregate Javascript files' setting at:
+ * admin/config/development/performance.
+ * - Load the page: examples/js_example/weights. Examine the page source.
+ * You will see that the color js scripts have been added in the
+ * element in weight order.
+ *
+ * To test further, change a weight in the $weights array below and in library
+ * yml file, then rebuild cache and reload examples/js_example/weights.
+ * Examine the new source to see the reordering.
+ *
+ * @return array
+ * A renderable array.
+ */
+ public function getJsWeightImplementation() {
+ // Create an array of items with random-ish weight values.
+ $weights = [
+ 'red' => -4,
+ 'blue' => -2,
+ 'green' => -1,
+ 'brown' => -2,
+ 'black' => -1,
+ 'purple' => -5,
+ ];
+
+ // Start building the content.
+ $build = [];
+ // Main container DIV. We give it a unique ID so that the JavaScript can
+ // find it using jQuery.
+ $build['content'] = [
+ '#markup' => '',
+ ];
+ // Attach library containing css and js files.
+ $build['#attached']['library'][] = 'js_example/js_example.weights';
+ // Attach the weights array to our JavaScript settings. This allows the
+ // color scripts we just attached to discover their weight values, by
+ // accessing drupalSettings.js_example.js_weights.*color*. The color scripts
+ // only use this information for display to the user.
+ $build['#attached']['drupalSettings']['js_example']['js_weights'] = $weights;
+
+ return $build;
+ }
+
+ /**
+ * Accordion page implementation.
+ *
+ * We're allowing a twig template to define our content in this case,
+ * which isn't normally how things work, but it's easier to demonstrate
+ * the JavaScript this way.
+ *
+ * @return array
+ * A renderable array.
+ */
+ public function getJsAccordionImplementation() {
+ $title = t('Click sections to expand or collapse:');
+ // Build using our theme. This gives us content, which is not a good
+ // practice, but which allows us to demonstrate adding JavaScript here.
+ $build['myelement'] = [
+ '#theme' => 'js_example_accordion',
+ '#title' => $title,
+ ];
+ // Add our script. It is tiny, but this demonstrates how to add it. We pass
+ // our module name followed by the internal library name declared in
+ // libraries yml file.
+ $build['myelement']['#attached']['library'][] = 'js_example/js_example.accordion';
+ // Return the renderable array.
+ return $build;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/js_example/templates/accordion.html.twig b/sites/all/modules/examples/examples/js_example/templates/accordion.html.twig
new file mode 100755
index 000000000..6c54e950c
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/templates/accordion.html.twig
@@ -0,0 +1,60 @@
+{#
+
+/**
+ * @file
+ * Template file for js_example module.
+ */
+#}
+
+
+ Mauris mauris ante, blandit et, ultrices a, suscipit eget, quam. Integer
+ ut neque. Vivamus nisi metus, molestie vel, gravida in, condimentum sit
+ amet, nunc. Nam a nibh. Donec suscipit eros. Nam mi. Proin viverra leo ut
+ odio. Curabitur malesuada. Vestibulum a velit eu ante scelerisque vulputate.
+
+ Sed non urna. Donec et ante. Phasellus eu ligula. Vestibulum sit amet
+ purus. Vivamus hendrerit, dolor at aliquet laoreet, mauris turpis porttitor
+ velit, faucibus interdum tellus libero ac justo. Vivamus non quam. In
+ suscipit faucibus urna.
+
+ Nam enim risus, molestie et, porta ac, aliquam ac, risus. Quisque lobortis.
+ Phasellus pellentesque purus in massa. Aenean in pede. Phasellus ac libero
+ ac tellus pellentesque semper. Sed ac felis. Sed commodo, magna quis
+ lacinia ornare, quam ante aliquam nisi, eu iaculis leo purus venenatis dui.
+
+ Cras dictum. Pellentesque habitant morbi tristique senectus et netus
+ et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in
+ faucibus orci luctus et ultrices posuere cubilia Curae; Aenean lacinia
+ mauris vel est.
+
+
+ Suspendisse eu nisl. Nullam ut libero. Integer dignissim consequat lectus.
+ Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
+ inceptos himenaeos.
+
+
+
+
+
diff --git a/sites/all/modules/examples/examples/js_example/templates/description.html.twig b/sites/all/modules/examples/examples/js_example/templates/description.html.twig
new file mode 100644
index 000000000..425ee7fee
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/templates/description.html.twig
@@ -0,0 +1,23 @@
+{#
+
+Description text for the JavaScript Example.
+
+#}
+
+{% trans %}
+
+
Drupal includes jQuery and jQuery UI.
+
+
We have two examples of using these:
+
+
+
+
An accordion-style section reveal effect: This demonstrates calling a jQuery
+ UI function using Drupal rendering system.
+
+
+
Sorting according to numeric weight: This demonstrates attaching your own
+ JavaScript code to individual page elements using Drupal rendering system.
+
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/js_example/tests/src/Functional/JsExampleTest.php b/sites/all/modules/examples/examples/js_example/tests/src/Functional/JsExampleTest.php
new file mode 100644
index 000000000..1e3361ced
--- /dev/null
+++ b/sites/all/modules/examples/examples/js_example/tests/src/Functional/JsExampleTest.php
@@ -0,0 +1,40 @@
+assertSession();
+
+ $paths = [
+ 'examples/js-example',
+ 'examples/js-example/weights',
+ 'examples/js-example/accordion',
+ ];
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ $assert->statusCodeEquals(200);
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.basic_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.basic_content_type.default.yml
new file mode 100644
index 000000000..b6d281c53
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.basic_content_type.default.yml
@@ -0,0 +1,64 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.node.basic_content_type.body
+ - node.type.basic_content_type
+ module:
+ - path
+ - text
+ enforced:
+ module:
+ - node_type_example
+id: node.basic_content_type.default
+targetEntityType: node
+bundle: basic_content_type
+mode: default
+content:
+ title:
+ type: string_textfield
+ weight: 0
+ settings:
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ uid:
+ type: entity_reference_autocomplete
+ weight: 1
+ settings:
+ match_operator: CONTAINS
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ created:
+ type: datetime_timestamp
+ weight: 2
+ settings: { }
+ third_party_settings: { }
+ promote:
+ type: boolean_checkbox
+ weight: 3
+ settings:
+ display_label: true
+ third_party_settings: { }
+ sticky:
+ type: boolean_checkbox
+ weight: 4
+ settings:
+ display_label: true
+ third_party_settings: { }
+ path:
+ type: path
+ weight: 5
+ settings: { }
+ third_party_settings: { }
+ body:
+ type: text_textarea_with_summary
+ weight: 6
+ settings:
+ rows: 9
+ summary_rows: 3
+ placeholder: ''
+ third_party_settings: { }
+hidden: { }
+third_party_settings: { }
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.locked_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.locked_content_type.default.yml
new file mode 100644
index 000000000..29050d4a2
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.locked_content_type.default.yml
@@ -0,0 +1,64 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.node.locked_content_type.body
+ - node.type.locked_content_type
+ module:
+ - path
+ - text
+ enforced:
+ module:
+ - node_type_example
+id: node.locked_content_type.default
+targetEntityType: node
+bundle: locked_content_type
+mode: default
+content:
+ title:
+ type: string_textfield
+ weight: 0
+ settings:
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ uid:
+ type: entity_reference_autocomplete
+ weight: 1
+ settings:
+ match_operator: CONTAINS
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ created:
+ type: datetime_timestamp
+ weight: 2
+ settings: { }
+ third_party_settings: { }
+ promote:
+ type: boolean_checkbox
+ weight: 3
+ settings:
+ display_label: true
+ third_party_settings: { }
+ sticky:
+ type: boolean_checkbox
+ weight: 4
+ settings:
+ display_label: true
+ third_party_settings: { }
+ path:
+ type: path
+ weight: 5
+ settings: { }
+ third_party_settings: { }
+ body:
+ type: text_textarea_with_summary
+ weight: 6
+ settings:
+ rows: 9
+ summary_rows: 3
+ placeholder: ''
+ third_party_settings: { }
+hidden: { }
+third_party_settings: { }
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.basic_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.basic_content_type.default.yml
new file mode 100644
index 000000000..e5977fc90
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.basic_content_type.default.yml
@@ -0,0 +1,26 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.node.basic_content_type.body
+ - node.type.basic_content_type
+ module:
+ - text
+ - user
+ enforced:
+ module:
+ - node_type_example
+id: node.basic_content_type.default
+targetEntityType: node
+bundle: basic_content_type
+mode: default
+content:
+ body:
+ label: hidden
+ type: text_default
+ weight: 101
+ settings: { }
+ third_party_settings: { }
+ links:
+ weight: 100
+hidden: { }
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.locked_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.locked_content_type.default.yml
new file mode 100644
index 000000000..b0e6b6a9a
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.locked_content_type.default.yml
@@ -0,0 +1,26 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.node.locked_content_type.body
+ - node.type.locked_content_type
+ module:
+ - text
+ - user
+ enforced:
+ module:
+ - node_type_example
+id: node.locked_content_type.default
+targetEntityType: node
+bundle: locked_content_type
+mode: default
+content:
+ body:
+ label: hidden
+ type: text_default
+ weight: 101
+ settings: { }
+ third_party_settings: { }
+ links:
+ weight: 100
+hidden: { }
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.basic_content_type.body.yml b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.basic_content_type.body.yml
new file mode 100644
index 000000000..d3d7f68b0
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.basic_content_type.body.yml
@@ -0,0 +1,25 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.node.body
+ - node.type.basic_content_type
+ module:
+ - text
+ enforced:
+ module:
+ - node_type_example
+id: node.basic_content_type.body
+field_name: body
+entity_type: node
+bundle: basic_content_type
+label: Body
+description: ''
+required: false
+translatable: true
+default_value: { }
+default_value_callback: ''
+settings:
+ display_summary: true
+third_party_settings: { }
+field_type: text_with_summary
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.locked_content_type.body.yml b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.locked_content_type.body.yml
new file mode 100644
index 000000000..4962e72b2
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.locked_content_type.body.yml
@@ -0,0 +1,25 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.node.body
+ - node.type.locked_content_type
+ module:
+ - text
+ enforced:
+ module:
+ - node_type_example
+id: node.locked_content_type.body
+field_name: body
+entity_type: node
+bundle: locked_content_type
+label: Body
+description: ''
+required: false
+translatable: true
+default_value: { }
+default_value_callback: ''
+settings:
+ display_summary: true
+third_party_settings: { }
+field_type: text_with_summary
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/node.type.basic_content_type.yml b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.basic_content_type.yml
new file mode 100644
index 000000000..8b8f33f6b
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.basic_content_type.yml
@@ -0,0 +1,14 @@
+langcode: en
+status: true
+dependencies:
+ enforced:
+ module:
+ - node_type_example
+name: 'Example: Basic Content Type'
+type: basic_content_type
+description: 'This is a very basic content type.'
+help: ''
+new_revision: false
+preview_mode: 1
+display_submitted: true
+third_party_settings: { }
diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/node.type.locked_content_type.yml b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.locked_content_type.yml
new file mode 100644
index 000000000..fb506be1f
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.locked_content_type.yml
@@ -0,0 +1,14 @@
+langcode: en
+status: true
+dependencies:
+ enforced:
+ module:
+ - node_type_example
+name: 'Example: Locked Content Type'
+type: locked_content_type
+description: 'Demo of a locked content type. Locking must be done in code. You must uninstall this module before you can delete the content type.'
+help: ''
+new_revision: false
+preview_mode: 1
+display_submitted: true
+third_party_settings: { }
diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.info.yml b/sites/all/modules/examples/examples/node_type_example/node_type_example.info.yml
new file mode 100644
index 000000000..77e5e588f
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.info.yml
@@ -0,0 +1,16 @@
+name: Node Type Example
+description: Demonstrates implementing a node content type in a module.
+package: Example modules
+type: module
+# core: 8.x
+dependencies:
+ - drupal:node
+ - drupal:path
+ - drupal:text
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.install b/sites/all/modules/examples/examples/node_type_example/node_type_example.install
new file mode 100644
index 000000000..c408989e4
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.install
@@ -0,0 +1,43 @@
+get('node.type.locked');
+ $locked['locked_content_type'] = 'locked_content_type';
+ Drupal::state()->set('node.type.locked', $locked);
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * Our content types will live on in the Drupal installation, even after this
+ * module is uninstalled. This is a good thing, since it allows the user to
+ * make decisions about their fate. Therefore we should give the user the
+ * option of deleting them.
+ *
+ * Since we told Drupal that our locked_content_type is locked, we now have
+ * to tell it to unlock.
+ *
+ * @ingroup node_type_example
+ */
+function node_type_example_uninstall() {
+ // Allow locked_content_type to be deleted.
+ $locked = Drupal::state()->get('node.type.locked');
+ unset($locked['locked_content_type']);
+ Drupal::state()->set('node.type.locked', $locked);
+}
diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.links.menu.yml b/sites/all/modules/examples/examples/node_type_example/node_type_example.links.menu.yml
new file mode 100644
index 000000000..476f03931
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.links.menu.yml
@@ -0,0 +1,3 @@
+node_type_example.description:
+ title: Config Node Type Example
+ route_name: config_node_type_example.description
diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.module b/sites/all/modules/examples/examples/node_type_example/node_type_example.module
new file mode 100644
index 000000000..22c0ce626
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.module
@@ -0,0 +1,44 @@
+Config Node Type Example is a basic example of defining a content type through configuration YAML files.
+
In this example we create two content types for Drupal 8, using only YAML files. Well, mostly only YAML files...
+ One of our content types is locked, so the user can't delete it while the module is installed. For this we need
+ a very tiny amount of support code.
The simplest way to author the per-type YAML files is to create the content types within Drupal and then take the
+ YAML files from the configuration directory. Like this:
+
+
Install Drupal 8.
+
Create a new content type at {{ add_types }}. Let's call it 'Nifty Content Type'.
+
Export the configuration from admin/config/development/configuration. Specific steps depending
+ on needs, and decisions made during Drupal 8 beta. You'll see a file called
+ node.type.nifty_content_type.yml.
+
Copy or move that file to your module's config/install directory, along with associated field
+ and form yml files.
+
Remove the uuid information from the configuration files.
+
+
You can see some of these YAML files in this module's config/install directory.
+
If you want to lock a content type created in this way, you'll have to implement hook_install() and
+ hook_uninstall(). In hook_install(), you'll set the content type to be locked.
+ In hook_uninstall() you'll set the content type to be unlocked.
+
Content types created in this way will remain available after the user has uninstalled the module.
+ If you were to fail to set the content type as unlocked, the user would not be able to delete it.
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/node_type_example/tests/src/Functional/NodeTypeExampleTest.php b/sites/all/modules/examples/examples/node_type_example/tests/src/Functional/NodeTypeExampleTest.php
new file mode 100644
index 000000000..fea8b4663
--- /dev/null
+++ b/sites/all/modules/examples/examples/node_type_example/tests/src/Functional/NodeTypeExampleTest.php
@@ -0,0 +1,175 @@
+ link relationships to check for.
+ * - The key is the path to the page where our link should appear.
+ * - The value is the link that should appear on that page.
+ */
+ protected function providerMenuLinks() {
+ return [
+ '' => '/examples/node-type-example',
+ ];
+ }
+
+ /**
+ * Verify and validate that default menu links were loaded for this module.
+ */
+ public function testNodeTypeExample() {
+ $assert = $this->assertSession();
+ // Test that our page loads.
+ $this->drupalGet('/examples/node-type-example');
+ $assert->statusCodeEquals(200);
+
+ // Test that our menu links were created.
+ $links = $this->providerMenuLinks();
+ foreach ($links as $page => $path) {
+ $this->drupalGet($page);
+ $assert->linkByHrefExists($path);
+ }
+ }
+
+ /**
+ * Test our new content types.
+ *
+ * Tests for the following:
+ *
+ * - That our content types appear in the user interface.
+ * - That our unlocked content type is unlocked.
+ * - That our locked content type is locked.
+ * - That we can create content using the user interface.
+ * - That our created content does appear in the database.
+ */
+ public function testNodeTypes() {
+ $assert = $this->assertSession();
+
+ // Log in an admin user.
+ $admin_user = $this->drupalCreateUser(['administer content types']);
+ $this->drupalLogin($admin_user);
+
+ // Get a list of content types.
+ $this->drupalGet('/admin/structure/types');
+ // Verify that these content types show up in the user interface.
+ $assert->pageTextContains('Example: Basic Content Type', 'Basic content type found.');
+ $assert->pageTextContains('Example: Locked Content Type', 'Locked content type found.');
+
+ // Check for the locked status of our content types.
+ // $nodeType will be of type Drupal\node\NodeTypeInterface.
+ $node_type = NodeType::load('basic_content_type');
+ $this->assertTrue($node_type, 'basic_content_type exists.');
+ if ($node_type) {
+ $this->assertFalse($node_type->isLocked(), 'basic_content_type is not locked.');
+ }
+ $node_type = NodeType::load('locked_content_type');
+ $this->assertTrue($node_type, 'locked_content_type exists.');
+ if ($node_type) {
+ $this->assertEquals('locked_content_type', $node_type->isLocked());
+ }
+
+ // Log in a content creator.
+ $creator_user = $this->drupalCreateUser(['create basic_content_type content']);
+ $this->drupalLogin($creator_user);
+
+ // Create a node.
+ $edit = [];
+ $edit['title[0][value]'] = $this->randomMachineName(8);
+ $edit['body[0][value]'] = $this->randomMachineName(16);
+ $this->drupalPostForm('/node/add/basic_content_type', $edit, 'Save');
+
+ // Check that the Basic page has been created.
+ $assert->pageTextContains(t('@post @title has been created.', [
+ '@post' => 'Example: Basic Content Type',
+ '@title' => $edit['title[0][value]'],
+ ]));
+
+ // Check that the node exists in the database.
+ $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
+ $this->assertTrue($node, 'Node found in database.');
+ }
+
+ /**
+ * Test that all fields are displayed when content is created.
+ */
+ public function testNodeCreation() {
+ // Login content creator.
+ $this->drupalLogin(
+ $this->drupalCreateUser([
+ 'create basic_content_type content',
+ 'create locked_content_type content',
+ ])
+ );
+
+ // Create random strings to insert data into fields.
+ $title = 'Test title.';
+ $body = 'Test body.';
+ $edit = [];
+ $edit['title[0][value]'] = $title;
+ $edit['body[0][value]'] = $body;
+
+ // Create a basic_content_type content.
+ $this->drupalPostForm('/node/add/basic_content_type', $edit, 'Save');
+ // Verify all fields and data of created content is shown.
+ $this->assertText($title);
+ $this->assertText($body);
+
+ // Create a locked_content_type content.
+ $this->drupalPostForm('/node/add/locked_content_type', $edit, 'Save');
+ // Verify all fields and data of created content is shown.
+ $this->assertText($title);
+ $this->assertText($body);
+ }
+
+
+ /**
+ * Verify that user can uninstall and then reinstall node_type_example.
+ */
+ public function testUninstallReinstall() {
+ $session = $this->assertSession();
+
+ // Uninstalling module.
+ $module_installer = $this->container->get('module_installer');
+ $module_installer->uninstall(['node_type_example']);
+ $this->drupalGet('examples/node-type-example');
+ $session->statusCodeEquals(404);
+
+ // Re-installing module.
+ $module_installer->install(['node_type_example']);
+ $this->drupalGet('examples/node-type-example');
+ $session->statusCodeEquals(200);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/page_example/page_example.info.yml b/sites/all/modules/examples/examples/page_example/page_example.info.yml
new file mode 100644
index 000000000..a560387e1
--- /dev/null
+++ b/sites/all/modules/examples/examples/page_example/page_example.info.yml
@@ -0,0 +1,14 @@
+name: Page Example
+type: module
+description: 'Demonstrates how to display a page at a given URL.'
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/page_example/page_example.links.menu.yml b/sites/all/modules/examples/examples/page_example/page_example.links.menu.yml
new file mode 100644
index 000000000..db95585a3
--- /dev/null
+++ b/sites/all/modules/examples/examples/page_example/page_example.links.menu.yml
@@ -0,0 +1,12 @@
+page_example.description:
+ title: Page Example
+ route_name: page_example_description
+ expanded: TRUE
+
+page_example.simple:
+ title: Simple - no arguments
+ route_name: page_example_simple
+ parent: page_example.description
+
+# We can't define a menu link for the page_example_arguments route, because it
+# requires path arguments.
diff --git a/sites/all/modules/examples/examples/page_example/page_example.module b/sites/all/modules/examples/examples/page_example/page_example.module
new file mode 100644
index 000000000..a1d0ab9d6
--- /dev/null
+++ b/sites/all/modules/examples/examples/page_example/page_example.module
@@ -0,0 +1,60 @@
+ '
' . $this->t('Simple page: The quick brown fox jumps over the lazy dog.') . '
',
+ ];
+ }
+
+ /**
+ * A more complex _controller callback that takes arguments.
+ *
+ * This callback is mapped to the path
+ * 'examples/page-example/arguments/{first}/{second}'.
+ *
+ * The arguments in brackets are passed to this callback from the page URL.
+ * The placeholder names "first" and "second" can have any value but should
+ * match the callback method variable names; i.e. $first and $second.
+ *
+ * This function also demonstrates a more complex render array in the returned
+ * values. Instead of rendering the HTML with theme('item_list'), content is
+ * left un-rendered, and the theme function name is set using #theme. This
+ * content will now be rendered as late as possible, giving more parts of the
+ * system a chance to change it if necessary.
+ *
+ * Consult @link http://drupal.org/node/930760 Render Arrays documentation
+ * @endlink for details.
+ *
+ * @param string $first
+ * A string to use, should be a number.
+ * @param string $second
+ * Another string to use, should be a number.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+ * If the parameters are invalid.
+ */
+ public function arguments($first, $second) {
+ // Make sure you don't trust the URL to be safe! Always check for exploits.
+ if (!is_numeric($first) || !is_numeric($second)) {
+ // We will just show a standard "access denied" page in this case.
+ throw new AccessDeniedHttpException();
+ }
+
+ $list[] = $this->t("First number was @number.", ['@number' => $first]);
+ $list[] = $this->t("Second number was @number.", ['@number' => $second]);
+ $list[] = $this->t('The total was @number.', ['@number' => $first + $second]);
+
+ $render_array['page_example_arguments'] = [
+ // The theme function to apply to the #items.
+ '#theme' => 'item_list',
+ // The list itself.
+ '#items' => $list,
+ '#title' => $this->t('Argument Information'),
+ ];
+ return $render_array;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/page_example/templates/description.html.twig b/sites/all/modules/examples/examples/page_example/templates/description.html.twig
new file mode 100644
index 000000000..e344cb72d
--- /dev/null
+++ b/sites/all/modules/examples/examples/page_example/templates/description.html.twig
@@ -0,0 +1,15 @@
+{#
+/**
+ * @file
+ * Contains the text of the page_example explanation page
+ */
+#}
+
+{% set page_example_simple = path('page_example_simple') %}
+{% set page_example_arguments = path('page_example_arguments', {'first': 23, 'second': 56}) %}
+
+{% trans %}
+
The Page example module provides two pages, "simple" and "arguments".
+
The simple page just returns a renderable array for display.
+
The arguments page takes two arguments and displays them, as in {{ page_example_arguments }}
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/page_example/tests/src/Functional/PageExampleTest.php b/sites/all/modules/examples/examples/page_example/tests/src/Functional/PageExampleTest.php
new file mode 100644
index 000000000..3e574f292
--- /dev/null
+++ b/sites/all/modules/examples/examples/page_example/tests/src/Functional/PageExampleTest.php
@@ -0,0 +1,176 @@
+drupalGet($url);
+ $this->assertSession()->statusCodeEquals(403);
+ }
+
+ /**
+ * Data provider for testing menu links.
+ *
+ * @return array
+ *
+ * Array of page -> link relationships to check for, with the permissions
+ * required to access them:
+ * - Permission machine name. Empty string means no login.
+ * - Array of link information:
+ * - Key is path to the page where the link should appear.
+ * - Value is the link that should appear on the page.
+ */
+ public function providerMenuLinks() {
+ return [
+ [
+ '',
+ ['' => '/examples/page-example'],
+ ],
+ [
+ 'access simple page',
+ ['/examples/page-example' => '/examples/page-example/simple'],
+ ],
+ ];
+ }
+
+ /**
+ * Verify and validate that default menu links were loaded for this module.
+ *
+ * @dataProvider providerMenuLinks
+ */
+ public function testPageExampleLinks($permission, $links) {
+ if ($permission) {
+ $user = $this->drupalCreateUser([$permission]);
+ $this->drupalLogin($user);
+ }
+ foreach ($links as $page => $path) {
+ $this->drupalGet($page);
+ $this->assertSession()->linkByHrefExists($path);
+ }
+ if ($permission) {
+ $this->drupalLogout();
+ }
+ }
+
+ /**
+ * Main test.
+ *
+ * Login user, create an example node, and test page functionality through
+ * the admin and user interfaces.
+ */
+ public function testPageExample() {
+ $assert_session = $this->assertSession();
+ // Verify that anonymous user can't access the pages created by
+ // page_example module.
+ $this->pageExampleVerifyNoAccess('examples/page-example/simple');
+ $this->pageExampleVerifyNoAccess('examples/page-example/arguments/1/2');
+
+ // Create a regular user and login.
+ $this->webUser = $this->drupalCreateUser();
+ $this->drupalLogin($this->webUser);
+
+ // Verify that regular user can't access the pages created by
+ // page_example module.
+ $this->pageExampleVerifyNoAccess('examples/page-example/simple');
+ $this->pageExampleVerifyNoAccess('examples/page-example/arguments/1/2');
+
+ // Create a user with permissions to access 'simple' page and login.
+ $this->webUser = $this->drupalCreateUser(['access simple page']);
+ $this->drupalLogin($this->webUser);
+
+ // Verify that user can access simple content.
+ $this->drupalGet('/examples/page-example/simple');
+ $assert_session->statusCodeEquals(200);
+ $assert_session->pageTextContains('The quick brown fox jumps over the lazy dog.');
+
+ // Check if user can't access arguments page.
+ $this->pageExampleVerifyNoAccess('examples/page-example/arguments/1/2');
+
+ // Create a user with permissions to access 'simple' page and login.
+ $this->webUser = $this->drupalCreateUser(['access arguments page']);
+ $this->drupalLogin($this->webUser);
+
+ // Verify that user can access arguments content.
+ $first = self::randomNumber(3);
+ $second = self::randomNumber(3);
+ $this->drupalGet('/examples/page-example/arguments/' . $first . '/' . $second);
+ $assert_session->statusCodeEquals(200);
+ // Verify argument usage.
+ $assert_session->pageTextContains(t('First number was @number.', ['@number' => $first]));
+ $assert_session->pageTextContains(t('Second number was @number.', ['@number' => $second]));
+ $assert_session->pageTextContains(t('The total was @number.', ['@number' => $first + $second]));
+
+ // Verify incomplete argument call to arguments content.
+ $this->drupalGet('/examples/page-example/arguments/' . $first . '/');
+ $assert_session->statusCodeEquals(404);
+
+ // Verify 403 for invalid second argument.
+ $this->drupalGet('/examples/page-example/arguments/' . $first . '/non-numeric-argument');
+ $assert_session->statusCodeEquals(403);
+
+ // Verify 403 for invalid first argument.
+ $this->drupalGet('/examples/page-example/arguments/non-numeric-argument/' . $second);
+ $assert_session->statusCodeEquals(403);
+
+ // Check if user can't access simple page.
+ $this->pageExampleVerifyNoAccess('examples/page-example/simple');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.info.yml b/sites/all/modules/examples/examples/pager_example/pager_example.info.yml
new file mode 100644
index 000000000..1cbc11116
--- /dev/null
+++ b/sites/all/modules/examples/examples/pager_example/pager_example.info.yml
@@ -0,0 +1,14 @@
+name: Pager Example
+type: module
+description: Demonstrates how to use table and render page elements to create paginated tables.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.links.menu.yml b/sites/all/modules/examples/examples/pager_example/pager_example.links.menu.yml
new file mode 100644
index 000000000..c34a73d32
--- /dev/null
+++ b/sites/all/modules/examples/examples/pager_example/pager_example.links.menu.yml
@@ -0,0 +1,3 @@
+pager_example.page:
+ title: Pager Example
+ route_name: pager_example.page
diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.module b/sites/all/modules/examples/examples/pager_example/pager_example.module
new file mode 100644
index 000000000..f9591fbf8
--- /dev/null
+++ b/sites/all/modules/examples/examples/pager_example/pager_example.module
@@ -0,0 +1,33 @@
+ [
+ 'template' => 'description',
+ 'variables' => [],
+ ],
+ ];
+}
+
+/**
+ * @} End of "defgroup pager_example".
+ */
diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.routing.yml b/sites/all/modules/examples/examples/pager_example/pager_example.routing.yml
new file mode 100644
index 000000000..fc255cbf9
--- /dev/null
+++ b/sites/all/modules/examples/examples/pager_example/pager_example.routing.yml
@@ -0,0 +1,6 @@
+pager_example.page:
+ path: 'examples/pager-example'
+ defaults:
+ _controller: '\Drupal\pager_example\Controller\PagerExamplePage::getContent'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/pager_example/src/Controller/PagerExamplePage.php b/sites/all/modules/examples/examples/pager_example/src/Controller/PagerExamplePage.php
new file mode 100644
index 000000000..1546f76e7
--- /dev/null
+++ b/sites/all/modules/examples/examples/pager_example/src/Controller/PagerExamplePage.php
@@ -0,0 +1,175 @@
+nodeStorage = $node_storage;
+ $this->currentUser = $current_user;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $controller = new static(
+ $container->get('entity_type.manager')->getStorage('node'),
+ $container->get('current_user')
+ );
+ $controller->setStringTranslation($container->get('string_translation'));
+ return $controller;
+ }
+
+ /**
+ * Content callback for the pager_example.page route.
+ */
+ public function getContent() {
+ // First we'll tell the user what's going on. This content can be found
+ // in the twig template file: templates/description.html.twig.
+ // @todo: Set up links to create nodes and point to devel module.
+ $build = [
+ 'description' => [
+ '#theme' => 'pager_example_description',
+ '#description' => 'foo',
+ '#attributes' => [],
+ ],
+ ];
+
+ // We need to count the number of nodes so that we can tell the user to add
+ // some if there aren't any.
+ $query = $this->nodeStorage->getQuery()
+ ->addTag('node_access')
+ ->count();
+
+ // The node_access tag does not trigger a check on whether a user has the
+ // ability to view unpublished content. The 'bypass node access' permission
+ // is really more than we need. But, there is no separate permission for
+ // viewing unpublished content. There is a permission to 'view own
+ // unpublished content', but we don't have a good way of using that in this
+ // query. So, unfortunately this query will incorrectly eliminate even those
+ // unpublished nodes that the user may, in fact, be allowed to view.
+ if (!$this->currentUser->hasPermission('bypass node access')) {
+ $query->condition('status', 1);
+ }
+ $count_nodes = $query->execute();
+
+ if ($count_nodes == 0) {
+ if ($this->currentUser->hasPermission('create page content')) {
+ $build['no-nodes'] = [
+ '#markup' => $this->t('There are no nodes to display.
+ Please create a node.',
+ [
+ ':url' => Url::fromRoute('node.add', ['node_type' => 'page'])->toString(),
+ ]
+ ),
+ ];
+ }
+ else {
+ $build['no-nodes'] = [
+ '#markup' => $this->t('There are no nodes to display.'),
+ ];
+ }
+
+ // Ensure that Drupal clears the cache when nodes have been published,
+ // unpublished, added or deleted; and when user permissions change.
+ $build['#cache']['tags'][] = 'node_list';
+ $build['#cache']['contexts'][] = 'user.permissions';
+ return $build;
+ }
+
+ // Now we want to get our tabular data. We select nodes from node storage
+ // limited by 2 per page and sort by nid DESC because we want to show newest
+ // node first. Additionally, we check that the user has permission to
+ // view the node.
+ $query = $this->nodeStorage->getQuery()
+ ->sort('nid', 'DESC')
+ ->addTag('node_access')
+ ->pager(2);
+
+ // The node_access tag does not trigger a check on whether a user has the
+ // ability to view unpublished content. The 'bypass node access' permission
+ // is really more than we need. But, there is no separate permission for
+ // viewing unpublished content. There is a permission to 'view own
+ // unpublished content', but we don't have a good way of using that in this
+ // query. So, unfortunately this query will incorrectly eliminate even those
+ // unpublished nodes that the user may, in fact, be allowed to view.
+ if (!$this->currentUser->hasPermission('bypass node access')) {
+ $query->condition('status', 1);
+ }
+ $entity_ids = $query->execute();
+
+ $nodes = $this->nodeStorage->loadMultiple($entity_ids);
+
+ // We are going to output the results in a table so we set up the rows.
+ $rows = [];
+ foreach ($nodes as $node) {
+ // There are certain things (besides unpublished nodes) that the
+ // node_access tag won't prevent from being seen. The only way to get at
+ // those is by explicitly checking for (view) access on a node-by-node
+ // basis. In order to prevent the pager from looking strange, we will
+ // "mask" these nodes that should not be accessible. If we don't do this
+ // masking, it's possible that we'd have lots of pages that don't show any
+ // content.
+ $rows[] = [
+ 'nid' => $node->access('view') ? $node->id() : t('XXXXXX'),
+ 'title' => $node->access('view') ? $node->getTitle() : t('Redacted'),
+ ];
+ }
+
+ // Build a render array which will be themed as a table with a pager.
+ $build['pager_example'] = [
+ '#rows' => $rows,
+ '#header' => [t('NID'), t('Title')],
+ '#type' => 'table',
+ '#empty' => t('No content available.'),
+ ];
+ $build['pager'] = [
+ '#type' => 'pager',
+ '#weight' => 10,
+ ];
+
+ // Ensure that Drupal clears the cache when nodes have been published,
+ // unpublished, added or deleted; and when user permissions change.
+ $build['#cache']['tags'][] = 'node_list';
+ $build['#cache']['contexts'][] = 'user.permissions';
+
+ return $build;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/pager_example/templates/description.html.twig b/sites/all/modules/examples/examples/pager_example/templates/description.html.twig
new file mode 100644
index 000000000..8deb25a16
--- /dev/null
+++ b/sites/all/modules/examples/examples/pager_example/templates/description.html.twig
@@ -0,0 +1,20 @@
+{#
+/**
+ * @file
+ * Contains the text of the pager_example explanation page
+ */
+#}
+
+
+{% trans %}
+
The Pager Example shows you how to create a paginated table. It uses
+ an Entity Query to retrieve nodes and the query checks that
+ the user has access to the nodes. You may test the access checking
+ feature by unpublishing some of your nodes and then viewing the
+ example as a user that is not allowed to see unpublished content.
+
In order to see this in action, make sure there are some nodes present in
+ your site. You can use the devel module to add some if needed, or just
+ create them.
+
The table on this page will show you two nodes per page.
+{% endtrans %}
+
diff --git a/sites/all/modules/examples/examples/pager_example/tests/src/Functional/PagerExampleTest.php b/sites/all/modules/examples/examples/pager_example/tests/src/Functional/PagerExampleTest.php
new file mode 100644
index 000000000..b4a2b7e9a
--- /dev/null
+++ b/sites/all/modules/examples/examples/pager_example/tests/src/Functional/PagerExampleTest.php
@@ -0,0 +1,68 @@
+drupalCreateUser();
+ $this->drupalLogin($normalUser);
+ }
+
+ /**
+ * Confirms nodes paging works correctly on page "pager_example".
+ */
+ public function testPagerExamplePage() {
+ $assert = $this->assertSession();
+
+ $nodes = [];
+ $nodes[] = $this->drupalCreateNode();
+
+ $this->drupalGet('examples/pager-example');
+ $assert->linkNotExists('Next');
+ $assert->linkNotExists('Previous');
+
+ // Create 5 new nodes.
+ for ($i = 1; $i <= 5; $i++) {
+ $nodes[] = $this->drupalCreateNode([
+ 'title' => "Node number $i",
+ ]);
+ }
+
+ // The pager pages are cached, so flush to see the 5 more nodes.
+ drupal_flush_all_caches();
+
+ // Check 'Next' link on first page.
+ $this->drupalGet('examples/pager-example');
+ $assert->statusCodeEquals(200);
+ $assert->linkByHrefExists('?page=1');
+ $assert->pageTextContains($nodes[5]->getTitle());
+
+ // Check the last page.
+ $this->drupalGet('examples/pager-example', ['query' => ['page' => 2]]);
+ $assert->statusCodeEquals(200);
+ $assert->linkNotExists('Next');
+ $assert->linkByHrefExists('?page=1');
+ $assert->pageTextContains($nodes[1]->getTitle());
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/phpcs.xml.dist b/sites/all/modules/examples/examples/phpcs.xml.dist
new file mode 100644
index 000000000..0bb25b9be
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpcs.xml.dist
@@ -0,0 +1,68 @@
+
+
+
+ Default PHP CodeSniffer configuration for Examples for Developers.
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/all/modules/examples/examples/phpunit_example/phpunit_example.info.yml b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.info.yml
new file mode 100644
index 000000000..c99fe64d0
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.info.yml
@@ -0,0 +1,14 @@
+name: PHPUnit Example
+type: module
+description: Demontrates how to use PHPUnit-based tests.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/phpunit_example/phpunit_example.links.menu.yml b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.links.menu.yml
new file mode 100644
index 000000000..ec4b30eca
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.links.menu.yml
@@ -0,0 +1,3 @@
+phpunit_example.description:
+ title: PHPUnit Example
+ route_name: phpunit_example_description
diff --git a/sites/all/modules/examples/examples/phpunit_example/phpunit_example.module b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.module
new file mode 100644
index 000000000..b02383de5
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.module
@@ -0,0 +1,17 @@
+items[$item->getDisplayName()] = $item;
+ }
+
+ /**
+ * A count of how many items exist.
+ *
+ * @return int
+ * The number of items that exist.
+ */
+ public function countDisplayableItems() {
+ return count($this->items);
+ }
+
+ /**
+ * All displayable items.
+ *
+ * @return array
+ * The displayable items.
+ */
+ public function displayableItems() {
+ return $this->items;
+ }
+
+ /**
+ * Find an item by its name.
+ *
+ * @param string $name
+ * The name to find.
+ *
+ * @return DisplayInfoInterface|null
+ * The found item, or NULL if none is found.
+ */
+ public function item($name) {
+ if (isset($this->items[$name])) {
+ return $this->items[$name];
+ }
+ return NULL;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/phpunit_example/src/ProtectedPrivates.php b/sites/all/modules/examples/examples/phpunit_example/src/ProtectedPrivates.php
new file mode 100644
index 000000000..0a2167d90
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/src/ProtectedPrivates.php
@@ -0,0 +1,58 @@
+add($a, $b);
+ }
+
+ /**
+ * A simple addition method with validity checking.
+ *
+ * @param int|float $a
+ * A number to add.
+ * @param int|float $b
+ * Another number to add.
+ *
+ * @return numeric
+ * The sum of $a and $b.
+ *
+ * @throws \InvalidArgumentException
+ * If either $a or $b is non-numeric, we can't add, so we throw.
+ */
+ private function privateAdd($a, $b) {
+ $adder = new AddClass();
+ return $adder->add($a, $b);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/phpunit_example/templates/description.html.twig b/sites/all/modules/examples/examples/phpunit_example/templates/description.html.twig
new file mode 100644
index 000000000..249068771
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/templates/description.html.twig
@@ -0,0 +1,55 @@
+{#
+
+Description text for the PHPUnit Example.
+
+#}
+
+{% trans %}
+
PHPUnit for Drupal: A very basic how-to.
+
+
How to use this example module
+
You really should be reading the various docblocks in the test files.
+
+
How To:
+
+
PHPUnit tests belong in their own directory, so they won't be loaded
+ by the autoloader during normal bootstrap. This means you should have a
+ /tests/src directory in the root of your module directory.
+
+
+
Your tests should be in the Drupal\Tests\[your_module]\Unit
+ namespace. Under Drupal's PSR-4 system, this means your PHPUnit-based
+ tests should go in a [your_module]/tests/src/Unit
+ directory.
+
+
Your test case should subclass Drupal\Tests\UnitTestCase.
+
+
+
+
You can run PHPUnit-based tests from within Drupal 8 by enabling the
+ Testing module and then selecting the PHPUnit group from the testing page.
+ As of this writing, this method doesn't provide any useful output.
+
+
+
+
+
Standard PHPUnit Practices
+
You can (and really, should) run PHPUnit from the command line.
+
On unix-based systems this means you need to cd core and then
+
./vendor/bin/phpunit
+
+
On Windows-based systems, assuming you have php in your path,
+
Also, you should mark your tests as belonging to a group, so they can be run
+ independently. You do this by annotating your test classes with
+ @group group_name. You should have a @group for
+ your module name, and you should also have a @group for
+ integrations, such as views.
+
+
So, for instance, to run all of the PHPUnit example tests, you would type
+
./vendor/bin/phpunit --group phpunit_example
+
+
As you can see, including a @group annotation is a good idea.
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php
new file mode 100644
index 000000000..75f9f2ce4
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php
@@ -0,0 +1,70 @@
+ link relationships to check for.
+ * The key is the path to the page where our link should appear.
+ * The value is the link that should appear on that page.
+ */
+ protected function providerMenuLinks() {
+ return [
+ '' => '/examples/phpunit-example',
+ ];
+ }
+
+ /**
+ * Verify and validate that default menu links were loaded for this module.
+ */
+ public function testPhpUnitExampleLink() {
+ $links = $this->providerMenuLinks();
+ foreach ($links as $page => $path) {
+ $this->drupalGet($page);
+ $this->assertLinkByHref($path);
+ }
+ }
+
+ /**
+ * Tests phpunit_example menus.
+ */
+ public function testPhpUnitExampleMenu() {
+ $this->drupalGet('/examples/phpunit-example');
+ $this->assertResponse(200, 'Description page exists.');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/AddClassTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/AddClassTest.php
new file mode 100644
index 000000000..4d5d0cae6
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/AddClassTest.php
@@ -0,0 +1,180 @@
+assertEquals($sut->add(2, 3), 5);
+ }
+
+ /**
+ * Test AddClass::add() with a data provider method.
+ *
+ * This method is very similar to testAdd(), but uses a data provider method
+ * to test with a wider range of data.
+ *
+ * You can tell PHPUnit which method is the data provider using the
+ * '@dataProvider' annotation.
+ *
+ * The data provider method just returns a big array of arrays of arguments.
+ * That is, for each time you want this test method run, the data provider
+ * should create an array of arguments for this method. In this case, it's
+ * $expected, $a, and $b. So one set of arguments would look a bit like this
+ * pseudocode:
+ *
+ * @code
+ * array( valueForExpected, valueForA, valueForB )
+ * @endcode
+ *
+ * It would then wrap this up in a higher-level array, so that PHPUnit can
+ * loop through them, like this pseudocode:
+ *
+ * @code
+ * return array( array(first, set), array (next, set) );
+ * @endcode
+ *
+ * This test has a better methodology than testAdd(), because it can easily
+ * be adapted by other developers, and because it tries more than one data
+ * set. This test is much better than testAdd(), although it still only
+ * tests 'good' data. When combined with testAddWithBadDataProvider(),
+ * we get a better picture of the behavior of the method under test.
+ *
+ * @dataProvider addDataProvider
+ *
+ * @see AddClassTest::addDataProvider()
+ */
+ public function testAddWithDataProvider($expected, $a, $b) {
+ $sut = new AddClass();
+ $this->assertEquals($expected, $sut->add($a, $b));
+ }
+
+ /**
+ * Test AddClass::add() with data that should throw an exception.
+ *
+ * This method is similar to testAddWithDataProvider(), but the data
+ * provider gives us data that should throw an exception.
+ *
+ * This test uses the '@expectedException' annotation to tell PHPUnit that
+ * a thrown exception should pass the test. You specify a
+ * fully-qualified exception class name. If you specify \Exception, PHPUnit
+ * will pass any exception, whereas a more specific subclass of \Exception
+ * will require that exception type to be thrown.
+ *
+ * Alternately, you can use try and catch blocks with assertions in order
+ * to test exceptions. We won't demonstrate that here; it's a much better
+ * idea to test your exceptions with @expectedException.
+ *
+ * @dataProvider addBadDataProvider
+ *
+ * @expectedException \InvalidArgumentException
+ *
+ * @see AddClassTest::addBadDataProvider()
+ */
+ public function testAddWithBadDataProvider($a, $b) {
+ $sut = new AddClass();
+ $sut->add($a, $b);
+ }
+
+ /**
+ * Data provider for testAddWithDataProvider().
+ *
+ * Data provider methods take no arguments and return an array of data
+ * to use for tests. Each element of the array is another array, which
+ * corresponds to the arguments in the test method's signature.
+ *
+ * Note also that PHPUnit tries to run tests using methods that begin
+ * with 'test'. This means that data provider method names should not
+ * begin with 'test'. Also, by convention, they should end with
+ * 'DataProvider'.
+ *
+ * @return array
+ * Nested arrays of values to check:
+ * - $a
+ * - $b
+ * - $expected
+ *
+ * @see AddClassTest::testAddWithDataProvider()
+ */
+ public function addDataProvider() {
+ return [
+ [5, 2, 3],
+ [50, 20, 30],
+ ];
+ }
+
+ /**
+ * Data provider for testAddWithBadDataProvider().
+ *
+ * Since AddClass::add() can throw exceptions, it's time
+ * to give it some data that will cause these exceptions.
+ *
+ * add() should throw exceptions if either of it's arguments are
+ * not numeric, and we will generate some test data to prove that
+ * this is what it actually does.
+ *
+ * @see AddClassTest::testAddWithBadDataProvider()
+ */
+ public function addBadDataProvider() {
+ $bad_data = [];
+ // Set up an array with data that should cause add()
+ // to throw an exception.
+ $bad_data_types = ['string', FALSE, ['foo'], new \stdClass()];
+ // Create some data where both $a and $b are bad types.
+ foreach ($bad_data_types as $bad_datum_a) {
+ foreach ($bad_data_types as $bad_datum_b) {
+ $bad_data[] = [$bad_datum_a, $bad_datum_b];
+ }
+ }
+ // Create some data where $a is good and $b is bad.
+ foreach ($bad_data_types as $bad_datum_b) {
+ $bad_data[] = [1, $bad_datum_b];
+ }
+ // Create some data where $b is good and $a is bad.
+ foreach ($bad_data_types as $bad_datum_a) {
+ $bad_data[] = [$bad_datum_a, 1];
+ }
+ return $bad_data;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/DisplayManagerTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/DisplayManagerTest.php
new file mode 100644
index 000000000..628ec4329
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/DisplayManagerTest.php
@@ -0,0 +1,59 @@
+getMock('Drupal\phpunit_example\DisplayInfoInterface');
+ // Here we're illustrating that the mock object belongs to
+ // our interface.
+ $this->assertTrue($mock instanceof DisplayInfoInterface);
+ // 'Program' our mock object to return a value for getDisplayName().
+ // expects($this->any()) tells the mock to return this value any time
+ // the method is called.
+ $mock->expects($this->any())
+ ->method('getDisplayName')
+ ->will($this->returnValue('the display name'));
+
+ // Create a DisplayManager, the class we're actually testing here.
+ $dm = new DisplayManager();
+ // Give it the mocked info object.
+ $dm->addDisplayableItem($mock);
+ // Assert that our DisplayManager has exactly one display object (our mock).
+ $this->assertEquals(1, $dm->countDisplayableItems());
+ // Assert that the DisplayManager can find our mocked info object.
+ $this->assertSame($mock, $dm->item('the display name'));
+ // Assert that the DisplayManager can't find an info object
+ // that it shouldn't have.
+ $this->assertNull($dm->item('nonexistant'));
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/ProtectedPrivatesTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/ProtectedPrivatesTest.php
new file mode 100644
index 000000000..e67bfbd9c
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/ProtectedPrivatesTest.php
@@ -0,0 +1,149 @@
+getMethod($method_name);
+ $method->setAccessible(TRUE);
+ return $method;
+ }
+
+ /**
+ * Good data provider.
+ */
+ public function addDataProvider() {
+ return [
+ [5, 2, 3],
+ ];
+ }
+
+ /**
+ * Test ProtectedPrivate::privateAdd().
+ *
+ * We want to test a private method on a class. This is problematic
+ * because, by design, we don't have access to this method. However,
+ * we do have a tool available to help us out with this problem:
+ * We can override the accessibility of a method using reflection.
+ *
+ * @dataProvider addDataProvider
+ */
+ public function testPrivateAdd($expected, $a, $b) {
+ // Get a reflected, accessible version of the privateAdd() method.
+ $private_method = $this->getAccessibleMethod(
+ 'Drupal\phpunit_example\ProtectedPrivates',
+ 'privateAdd'
+ );
+ // Create a new ProtectedPrivates object.
+ $pp = new ProtectedPrivates();
+ // Use the reflection to invoke on the object.
+ $sum = $private_method->invokeArgs($pp, [$a, $b]);
+ // Make an assertion.
+ $this->assertEquals($expected, $sum);
+ }
+
+ /**
+ * Bad data provider.
+ */
+ public function addBadDataProvider() {
+ return [
+ ['string', []],
+ ];
+ }
+
+ /**
+ * Test ProtectedPrivate::privateAdd() with bad data.
+ *
+ * This is essentially the same test as testPrivateAdd(), but using
+ * non-numeric data. This lets us test the exception-throwing ability
+ * of this private method.
+ *
+ * @expectedException \InvalidArgumentException
+ *
+ * @dataProvider addBadDataProvider
+ */
+ public function testPrivateAddBadData($a, $b) {
+ // Get a reflected, accessible version of the privateAdd() method.
+ $private_method = $this->getAccessibleMethod(
+ 'Drupal\phpunit_example\ProtectedPrivates',
+ 'privateAdd');
+ // Create a new ProtectedPrivates object.
+ $pp = new ProtectedPrivates();
+ // Use the reflection to invoke on the object.
+ // This should throw an exception.
+ $private_method->invokeArgs($pp, [$a, $b]);
+ }
+
+ /**
+ * Test ProtectedPrivates::protectedAdd() using a stub class.
+ *
+ * We could use the same reflection technique to test protected
+ * methods, just like we did with private ones.
+ *
+ * But sometimes it might make more sense to use a stub class
+ * which will have access to the protected method. That's what
+ * we'll demonstrate here.
+ *
+ * @dataProvider addDataProvider
+ */
+ public function testProtectedAdd($expected, $a, $b) {
+ $stub = new ProtectedPrivatesSubclass();
+ $this->assertEquals($expected, $stub->subclassProtectedAdd($a, $b));
+ }
+
+ /**
+ * Test ProtectedPrivates::protectedAdd() with bad data using a stub class.
+ *
+ * This test is similar to testProtectedAdd(), but expects an exception.
+ *
+ * @expectedException \InvalidArgumentException
+ *
+ * @dataProvider addBadDataProvider
+ */
+ public function testProtectedAddBadData($a, $b) {
+ $stub = new ProtectedPrivatesSubclass();
+ $stub->subclassProtectedAdd($a, $b);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/Subclasses/ProtectedPrivatesSubclass.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/Subclasses/ProtectedPrivatesSubclass.php
new file mode 100644
index 000000000..a4b5bcb7e
--- /dev/null
+++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/Subclasses/ProtectedPrivatesSubclass.php
@@ -0,0 +1,30 @@
+protectedAdd($a, $b);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.api.php b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.api.php
new file mode 100644
index 000000000..e3f6e3a3c
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.api.php
@@ -0,0 +1,24 @@
+ $plugin_definition) {
+ $sandwich_plugin_info[$plugin_id]['foobar'] = t('We have altered this in the alter hook');
+ }
+}
diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.info.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.info.yml
new file mode 100644
index 000000000..242fbff80
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.info.yml
@@ -0,0 +1,14 @@
+name: Plugin Type Example
+type: module
+description: Provides an example of defining a plugin type.
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.links.menu.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.links.menu.yml
new file mode 100644
index 000000000..c607e3627
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.links.menu.yml
@@ -0,0 +1,3 @@
+plugin_type_example.description:
+ title: Plugin Type Example
+ route_name: plugin_type_example.description
diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.module b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.module
new file mode 100644
index 000000000..eea5ec8a9
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.module
@@ -0,0 +1,71 @@
+ $plugin_definition) {
+ // Always uppercase the word rocket in sandwich descriptions.
+ $sandwich_plugin_info[$plugin_id]['description'] = str_replace(t('rocket'), t('ROCKET'), $sandwich_plugin_info[$plugin_id]['description']);
+ }
+}
diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.routing.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.routing.yml
new file mode 100644
index 000000000..35d6acfde
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.routing.yml
@@ -0,0 +1,8 @@
+# This defines our example page's path to the routing system.
+plugin_type_example.description:
+ path: '/examples/plugin-type-example'
+ defaults:
+ _controller: '\Drupal\plugin_type_example\Controller\PluginTypeExampleController::description'
+ _title: 'Plugin Type Example'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.services.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.services.yml
new file mode 100644
index 000000000..db658ff56
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.services.yml
@@ -0,0 +1,11 @@
+# This declares the plugin manager to the service container. For background
+# information on the service container, see https://www.drupal.org/node/2133171.
+# Changes here require that the cache be cleared in order to have Drupal notice
+# them.
+services:
+ # The machine name of the service. This is the string that must be passed to
+ # Drupal::service() to get the instantiated plugin manager.
+ plugin.manager.sandwich:
+ # This tells the service container the name of our plugin manager class.
+ class: Drupal\plugin_type_example\SandwichPluginManager
+ parent: default_plugin_manager
diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/Annotation/Sandwich.php b/sites/all/modules/examples/examples/plugin_type_example/src/Annotation/Sandwich.php
new file mode 100644
index 000000000..29a7f65ce
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/src/Annotation/Sandwich.php
@@ -0,0 +1,47 @@
+sandwichManager = $sandwich_manager;
+ }
+
+ /**
+ * Displays a page with an overview of our plugin type and plugins.
+ *
+ * Lists all the Sandwich plugin definitions by using methods on the
+ * \Drupal\plugin_type_example\SandwichPluginManager class. Lists out the
+ * description for each plugin found by invoking methods defined on the
+ * plugins themselves. You can find the plugins we have defined in the
+ * \Drupal\plugin_type_example\Plugin\Sandwich namespace.
+ *
+ * @return array
+ * Render API array with content for the page at
+ * /examples/plugin_type_example.
+ */
+ public function description() {
+ $build = [];
+
+ $build['intro'] = [
+ '#markup' => t("This page lists the sandwich plugins we've created. The sandwich plugin type is defined in Drupal\\plugin_type_example\\SandwichPluginManager. The various plugins are defined in the Drupal\\plugin_type_example\\Plugin\\Sandwich namespace."),
+ ];
+
+ // Get the list of all the sandwich plugins defined on the system from the
+ // plugin manager. Note that at this point, what we have is *definitions* of
+ // plugins, not the plugins themselves.
+ $sandwich_plugin_definitions = $this->sandwichManager->getDefinitions();
+
+ // Let's output a list of the plugin definitions we now have.
+ $items = [];
+ foreach ($sandwich_plugin_definitions as $sandwich_plugin_definition) {
+ // Here we use various properties from the plugin definition. These values
+ // are defined in the annotation at the top of the plugin class: see
+ // \Drupal\plugin_type_example\Plugin\Sandwich\ExampleHamSandwich.
+ $items[] = t("@id (calories: @calories, description: @description )", [
+ '@id' => $sandwich_plugin_definition['id'],
+ '@calories' => $sandwich_plugin_definition['calories'],
+ '@description' => $sandwich_plugin_definition['description'],
+ ]);
+ }
+
+ // Add our list to the render array.
+ $build['plugin_definitions'] = [
+ '#theme' => 'item_list',
+ '#title' => 'Sandwich plugin definitions',
+ '#items' => $items,
+ ];
+
+ // If we want just a single plugin definition, we can use getDefinition().
+ // This requires us to know the ID of the plugin we want. This is set in the
+ // annotation on the plugin class: see
+ // \Drupal\plugin_type_example\Plugin\Sandwich\ExampleHamSandwich.
+ $ham_sandwich_plugin_definition = $this->sandwichManager->getDefinition('meatball_sandwich');
+
+ // To get an instance of a plugin, we call createInstance() on the plugin
+ // manager, passing the ID of the plugin we want to load. Let's output a
+ // list of the plugins by loading an instance of each plugin definition and
+ // collecting the description from each.
+ $items = [];
+ // The array of plugin definitions is keyed by plugin id, so we can just use
+ // that to load our plugin instances.
+ foreach ($sandwich_plugin_definitions as $plugin_id => $sandwich_plugin_definition) {
+ // We now have a plugin instance. From here on it can be treated just as
+ // any other object; have its properties examined, methods called, etc.
+ $plugin = $this->sandwichManager->createInstance($plugin_id, ['of' => 'configuration values']);
+ $items[] = $plugin->description();
+ }
+
+ $build['plugins'] = [
+ '#theme' => 'item_list',
+ '#title' => 'Sandwich plugins',
+ '#items' => $items,
+ ];
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Override the parent method so that we can inject our sandwich plugin
+ * manager service into the controller.
+ *
+ * For more about how dependency injection works read
+ * https://www.drupal.org/node/2133171
+ *
+ * @see container
+ */
+ public static function create(ContainerInterface $container) {
+ // Inject the plugin.manager.sandwich service that represents our plugin
+ // manager as defined in the plugin_type_example.services.yml file.
+ return new static($container->get('plugin.manager.sandwich'));
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php b/sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php
new file mode 100644
index 000000000..f9749cfc7
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php
@@ -0,0 +1,51 @@
+t() for string translations in our plugin.
+ use StringTranslationTrait;
+
+ /**
+ * The day the sandwich is ordered.
+ *
+ * Since meatball sandwiches have a special behavior on Sundays, and since we
+ * want to test that behavior on days other than Sunday, we have to store the
+ * day as a property so we can test it.
+ *
+ * This is the string representation of the day of the week you get from
+ * date('D').
+ *
+ * @var string
+ */
+ protected $day;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ // This class needs to translate strings, so we need to inject the string
+ // translation service from the container. This means our plugin class has
+ // to implement ContainerFactoryPluginInterface. This requires that we make
+ // this create() method, and use it to inject services from the container.
+ // @see https://www.drupal.org/node/2012118
+ $sandwich = new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('string_translation')
+ );
+ return $sandwich;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, TranslationInterface $translation) {
+ // Store the translation service.
+ $this->setStringTranslation($translation);
+ // Store the day so we can generate a special description on Sundays.
+ $this->day = date('D');
+ // Pass the other parameters up to the parent constructor.
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order(array $extras) {
+ $ingredients = ['meatballs', 'irresistible marinara sauce'];
+ $sandwich = array_merge($ingredients, $extras);
+ return 'You ordered an ' . implode(', ', $sandwich) . ' sandwich. Enjoy!';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function description() {
+ // We override the description() method in order to change the description
+ // text based on the date. On Sunday we only have day old bread.
+ if ($this->day == 'Sun') {
+ return $this->t("Italian style meatballs drenched in irresistible marinara sauce, served on day old bread.");
+ }
+ return parent::description();
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/SandwichBase.php b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichBase.php
new file mode 100644
index 000000000..8a1cfc844
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichBase.php
@@ -0,0 +1,53 @@
+pluginDefinition['description'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calories() {
+ // Retrieve the @calories property from the annotation and return it.
+ return (float) $this->pluginDefinition['calories'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ abstract public function order(array $extras);
+
+}
diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/SandwichInterface.php b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichInterface.php
new file mode 100644
index 000000000..e8a5fe3ef
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichInterface.php
@@ -0,0 +1,54 @@
+alterInfo('sandwich_info');
+
+ // This sets the caching method for our plugin definitions. Plugin
+ // definitions are discovered by examining the $subdir defined above, for
+ // any classes with an $plugin_definition_annotation_name. The annotations
+ // are read, and then the resulting data is cached using the provided cache
+ // backend. For our Sandwich plugin type, we've specified the @cache.default
+ // service be used in the plugin_type_example.services.yml file. The second
+ // argument is a cache key prefix. Out of the box Drupal with the default
+ // cache backend setup will store our plugin definition in the cache_default
+ // table using the sandwich_info key. All that is implementation details
+ // however, all we care about it that caching for our plugin definition is
+ // taken care of by this call.
+ $this->setCacheBackend($cache_backend, 'sandwich_info');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/plugin_type_example/tests/src/Functional/PluginTypeExampleTest.php b/sites/all/modules/examples/examples/plugin_type_example/tests/src/Functional/PluginTypeExampleTest.php
new file mode 100644
index 000000000..81a600781
--- /dev/null
+++ b/sites/all/modules/examples/examples/plugin_type_example/tests/src/Functional/PluginTypeExampleTest.php
@@ -0,0 +1,82 @@
+container->get('plugin.manager.sandwich');
+
+ $sandwich_plugin_definitions = $manager->getDefinitions();
+
+ $this->assertCount(2, $sandwich_plugin_definitions, 'There are not two sandwich plugins defined.');
+
+ // Check some of the properties of the ham sandwich plugin definition.
+ $sandwich_plugin_definition = $sandwich_plugin_definitions['ham_sandwich'];
+ $this->assertEquals(426, $sandwich_plugin_definition['calories'], 'The ham sandwich plugin definition\'s calories property is not set.');
+
+ // Create an instance of the ham sandwich plugin to check it works.
+ $plugin = $manager->createInstance('ham_sandwich', ['of' => 'configuration values']);
+
+ $this->assertInstanceOf(ExampleHamSandwich::class, $plugin);
+
+ // Create a meatball sandwich so we can check it's special behavior on
+ // Sundays.
+ /* @var $meatball \Drupal\plugin_type_example\SandwichInterface */
+ $meatball = $manager->createInstance('meatball_sandwich');
+ // Set the $day property to 'Sun'.
+ $ref_day = new \ReflectionProperty($meatball, 'day');
+ $ref_day->setAccessible(TRUE);
+ $ref_day->setValue($meatball, 'Sun');
+ // Check the special description on Sunday.
+ $this->assertEqual($meatball->description(), 'Italian style meatballs drenched in irresistible marinara sauce, served on day old bread.');
+ }
+
+ /**
+ * Test the output of the example page.
+ */
+ public function testPluginExamplePage() {
+ $assert = $this->assertSession();
+
+ $this->drupalGet('examples/plugin-type-example');
+ $assert->statusCodeEquals(200);
+
+ // Check we see the plugin id.
+ $assert->pageTextContains('ham_sandwich', 'The plugin ID was not output.');
+
+ // Check we see the plugin description.
+ $assert->pageTextContains('Ham, mustard, rocket, sun-dried tomatoes.', 'The plugin description was not output.');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/queue_example/queue_example.info.yml b/sites/all/modules/examples/examples/queue_example/queue_example.info.yml
new file mode 100644
index 000000000..4a087ee1d
--- /dev/null
+++ b/sites/all/modules/examples/examples/queue_example/queue_example.info.yml
@@ -0,0 +1,13 @@
+name: Queue Example
+type: module
+description: Examples of using the Drupal Queue API.
+package: Example modules
+# core: 8.x
+dependencies:
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/queue_example/queue_example.links.menu.yml b/sites/all/modules/examples/examples/queue_example/queue_example.links.menu.yml
new file mode 100644
index 000000000..29b86148c
--- /dev/null
+++ b/sites/all/modules/examples/examples/queue_example/queue_example.links.menu.yml
@@ -0,0 +1,3 @@
+queue_example:
+ title: Queue Example
+ route_name: queue_example
diff --git a/sites/all/modules/examples/examples/queue_example/queue_example.module b/sites/all/modules/examples/examples/queue_example/queue_example.module
new file mode 100644
index 000000000..b3b0712b4
--- /dev/null
+++ b/sites/all/modules/examples/examples/queue_example/queue_example.module
@@ -0,0 +1,37 @@
+queueFactory = $queue_factory;
+ $this->queueType = $settings->get('queue_default', 'queue.database');
+ $this->database = $database;
+ $this->cron = $cron;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static($container->get('queue'), $container->get('database'), $container->get('cron'), $container->get('settings'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ // Return a string that is the unique ID of our form. Best practice here is
+ // to namespace the form based on your module's name.
+ return 'queue_example';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ // Simple counter that makes it possible to put auto-incrementing default
+ // string into the string to insert.
+ if (empty($form_state->get('insert_counter'))) {
+ $form_state->set('insert_counter', 1);
+ }
+
+ $queue_name = $form_state->getValue('queue_name') ?: 'queue_example_first_queue';
+ $items = $this->retrieveQueue($queue_name);
+
+ $form['help'] = [
+ '#type' => 'markup',
+ '#markup' => '
' . $this->t('This page is an interface on the Drupal queue API. You can add new items to the queue, "claim" one (retrieve the next item and keep a lock on it), and delete one (remove it from the queue). Note that claims are not expired until cron runs, so there is a special button to run cron to perform any necessary expirations.') . '
' . $this->t('Note: the example works only with the default queue implementation, which is not currently configured!!') . '
',
+ '#access' => (!$this->doesQueueUseDB()),
+ ];
+
+ $queue_names = ['queue_example_first_queue', 'queue_example_second_queue'];
+ $form['queue_name'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Choose queue to examine'),
+ '#options' => array_combine($queue_names, $queue_names),
+ '#default_value' => $queue_name,
+ ];
+
+ $form['queue_show'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Show queue'),
+ '#submit' => [[$this, 'submitShowQueue']],
+ ];
+
+ $form['status_fieldset'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Queue status for @name', ['@name' => $queue_name]),
+ '#collapsible' => TRUE,
+ ];
+
+ if (count($items) > 0) {
+ $form['status_fieldset']['status'] = [
+ '#theme' => 'table',
+ '#header' => [
+ $this->t('Item ID'),
+ $this->t('Claimed/Expiration'),
+ $this->t('Created'),
+ $this->t('Content/Data'),
+ ],
+ '#rows' => array_map([$this, 'processQueueItemForTable'], $items),
+ ];
+ }
+ else {
+ $form['status_fieldset']['status'] = [
+ '#type' => 'markup',
+ '#markup' => $this->t('There are no items in the queue.'),
+ ];
+ }
+
+ $form['insert_fieldset'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Insert into @name', ['@name' => $queue_name]),
+ ];
+
+ $form['insert_fieldset']['string_to_add'] = [
+ '#type' => 'textfield',
+ '#size' => 10,
+ '#default_value' => $this->t('item @counter', ['@counter' => $form_state->get('insert_counter')]),
+ ];
+
+ $form['insert_fieldset']['add_item'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Insert into queue'),
+ '#submit' => [[$this, 'submitAddQueueItem']],
+ ];
+
+ $form['claim_fieldset'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Claim from queue'),
+ '#collapsible' => TRUE,
+ ];
+
+ $form['claim_fieldset']['claim_time'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Claim time, in seconds'),
+ '#options' => [
+ 0 => $this->t('none'),
+ 5 => $this->t('5 seconds'),
+ 60 => $this->t('60 seconds'),
+ ],
+ '#description' => $this->t('This time is only valid if cron runs during this time period. You can run cron manually below.'),
+ '#default_value' => $form_state->getValue('claim_time') ?: 5,
+ ];
+
+ $form['claim_fieldset']['claim_item'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Claim the next item from the queue'),
+ '#submit' => [[$this, 'submitClaimItem']],
+ ];
+
+ $form['claim_fieldset']['claim_and_delete_item'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Claim the next item and delete it'),
+ '#submit' => [[$this, 'submitClaimDeleteItem']],
+ ];
+
+ $form['claim_fieldset']['run_cron'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Run cron manually to expire claims'),
+ '#submit' => [[$this, 'submitRunCron']],
+ ];
+
+ $form['delete_queue'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Delete the queue and items in it'),
+ '#submit' => [[$this, 'submitDeleteQueue']],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ }
+
+ /**
+ * Retrieves the queue from the database for display purposes only.
+ *
+ * It is not recommended to access the database directly, and this is only
+ * here so that the user interface can give a good idea of what's going on
+ * in the queue.
+ *
+ * @param string $queue_name
+ * The name of the queue from which to fetch items.
+ *
+ * @return array
+ * An array of item arrays.
+ */
+ public function retrieveQueue($queue_name) {
+ $items = [];
+
+ // This example requires the default queue implementation to work,
+ // so we bail if some other queue implementation has been installed.
+ if (!$this->doesQueueUseDb()) {
+ return $items;
+ }
+
+ // Make sure there are queue items available. The queue will not create our
+ // database table if there are no items.
+ if ($this->queueFactory->get($queue_name)->numberOfItems() >= 1) {
+ $result = $this->database->query('SELECT item_id, data, expire, created FROM {' . DatabaseQueue::TABLE_NAME . '} WHERE name = :name ORDER BY item_id',
+ [':name' => $queue_name],
+ ['fetch' => \PDO::FETCH_ASSOC]
+ );
+ foreach ($result as $item) {
+ $items[] = $item;
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * Check if we are using the default database queue.
+ *
+ * @return bool
+ * TRUE if we are using the default database queue implementation.
+ */
+ protected function doesQueueUseDb() {
+ return $this->queueType == 'queue.database';
+ }
+
+ /**
+ * Submit function for the show-queue button.
+ *
+ * @param array $form
+ * Form definition array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state object.
+ */
+ public function submitShowQueue(array &$form, FormStateInterface $form_state) {
+ $queue = $this->queueFactory->get($form_state->getValue('queue_name'));
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+
+ // Get the number of items.
+ $count = $queue->numberOfItems();
+
+ // Update the form item counter.
+ $form_state->set('insert_counter', $count + 1);
+
+ // Unset the string_to_add textbox.
+ $form_state->unsetValue('string_to_add');
+
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Submit function for the insert-into-queue button.
+ *
+ * @param array $form
+ * Form definition array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state object.
+ */
+ public function submitAddQueueItem(array &$form, FormStateInterface $form_state) {
+ // Get a queue (of the default type) called 'queue_example_queue'.
+ // If the default queue class is SystemQueue this creates a queue that
+ // stores its items in the database.
+ $queue = $this->queueFactory->get($form_state->getValue('queue_name'));
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+
+ // Queue the string.
+ $queue->createItem($form_state->getValue('string_to_add'));
+ $count = $queue->numberOfItems();
+ drupal_set_message($this->t('Queued your string (@string_to_add). There are now @count items in the queue.', ['@count' => $count, '@string_to_add' => $form_state->getValue('string_to_add')]));
+ // Allows us to keep information in $form_state.
+ $form_state->setRebuild();
+
+ // Unsetting the string_to_add allows us to set the incremented default
+ // value for the user so they don't have to type anything.
+ $form_state->unsetValue('string_to_add');
+ $form_state->set('insert_counter', $count + 1);
+ }
+
+ /**
+ * Submit function for the "claim" button.
+ *
+ * Claims (retrieves) an item from the queue and reports the results.
+ *
+ * @param array $form
+ * Form definition array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state object.
+ */
+ public function submitClaimItem(array &$form, FormStateInterface $form_state) {
+ $queue = $this->queueFactory->get($form_state->getValue('queue_name'));
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $item = $queue->claimItem($form_state->getValue('claim_time'));
+ $count = $queue->numberOfItems();
+ if (!empty($item)) {
+ drupal_set_message($this->t('Claimed item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.',
+ [
+ '@count' => $count,
+ '@item_id' => $item->item_id,
+ '@string' => $item->data,
+ '@seconds' => $form_state->getValue('claim_time'),
+ ]));
+ }
+ else {
+ drupal_set_message($this->t('There were no items in the queue available to claim. There are @count items in the queue.', ['@count' => $count]));
+ }
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Submit function for "Claim and delete" button.
+ *
+ * @param array $form
+ * Form definition array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state object.
+ */
+ public function submitClaimDeleteItem(array &$form, FormStateInterface $form_state) {
+ $queue = $this->queueFactory->get($form_state->getValue('queue_name'));
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $count = $queue->numberOfItems();
+ $item = $queue->claimItem(60);
+ if (!empty($item)) {
+ drupal_set_message($this->t('Claimed and deleted item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.', [
+ '@count' => $count,
+ '@item_id' => $item->item_id,
+ '@string' => $item->data,
+ '@seconds' => $form_state->getValue('claim_time'),
+ ]));
+ $queue->deleteItem($item);
+ $count = $queue->numberOfItems();
+ drupal_set_message($this->t('There are now @count items in the queue.', ['@count' => $count]));
+ }
+ else {
+ $count = $queue->numberOfItems();
+ drupal_set_message($this->t('There were no items in the queue available to claim/delete. There are currently @count items in the queue.', ['@count' => $count]));
+ }
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Submit function for "run cron" button.
+ *
+ * Runs cron (to release expired claims) and reports the results.
+ *
+ * @param array $form
+ * Form definition array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state object.
+ */
+ public function submitRunCron(array &$form, FormStateInterface $form_state) {
+ $this->cron->run();
+ $queue = $this->queueFactory->get($form_state->getValue('queue_name'));
+ // @see https://www.drupal.org/node/2705809
+ if ($queue instanceof QueueGarbageCollectionInterface) {
+ $queue->garbageCollection();
+ }
+ // There is no harm in trying to recreate existing.
+ $queue->createQueue();
+ $count = $queue->numberOfItems();
+ drupal_set_message($this->t('Ran cron. If claimed items expired, they should be expired now. There are now @count items in the queue', ['@count' => $count]));
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Submit handler for clearing/deleting the queue.
+ *
+ * @param array $form
+ * Form definition array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state object.
+ */
+ public function submitDeleteQueue(array &$form, FormStateInterface $form_state) {
+ $queue = $this->queueFactory->get($form_state->getValue('queue_name'));
+ $queue->deleteQueue();
+ drupal_set_message($this->t('Deleted the @queue_name queue and all items in it', ['@queue_name' => $form_state->getValue('queue_name')]));
+ }
+
+ /**
+ * Helper method to format a queue item for display in a summary table.
+ *
+ * @param array $item
+ * Queue item array with keys for item_id, expire, created, and data.
+ *
+ * @return array
+ * An array with the queue properties in the right order for display in a
+ * summary table.
+ */
+ private function processQueueItemForTable(array $item) {
+ if ($item['expire'] > 0) {
+ $item['expire'] = $this->t('Claimed: expires %expire', ['%expire' => date('r', $item['expire'])]);
+ }
+ else {
+ $item['expire'] = $this->t('Unclaimed');
+ }
+ $item['created'] = date('r', $item['created']);
+ $item['content'] = Html::escape(unserialize($item['data']));
+ unset($item['data']);
+
+ return $item;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/queue_example/tests/src/Functional/QueueExampleTest.php b/sites/all/modules/examples/examples/queue_example/tests/src/Functional/QueueExampleTest.php
new file mode 100644
index 000000000..c7d809830
--- /dev/null
+++ b/sites/all/modules/examples/examples/queue_example/tests/src/Functional/QueueExampleTest.php
@@ -0,0 +1,68 @@
+drupalGet('examples/queue_example');
+ // Load the queue with 5 items.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = ['queue_name' => 'queue_example_first_queue', 'string_to_add' => 'boogie' . $i];
+ $this->drupalPostForm(NULL, $edit, 'Insert into queue');
+ $this->assertText(t('There are now @number items in the queue', ['@number' => $i]));
+ }
+ // Claim each of the 5 items with a claim time of 0 seconds.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0];
+ $this->drupalPostForm(NULL, $edit, 'Claim the next item from the queue');
+ $this->assertPattern(t('%Claimed item id=.*string=@string for 0 seconds.%', ['@string' => 'boogie' . $i]));
+ }
+ $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0];
+ $this->drupalPostForm(NULL, $edit, 'Claim the next item from the queue');
+ $this->assertText('There were no items in the queue available to claim');
+
+ // Sleep a second so we can make sure that the timeouts actually time out.
+ // Local systems work fine with this but apparently the PIFR server is so
+ // fast that it needs a sleep before the cron run.
+ sleep(1);
+
+ // Run cron to release expired items.
+ $this->drupalPostForm(NULL, [], 'Run cron manually to expire claims');
+
+ // Claim and delete each of the 5 items which should now be available.
+ for ($i = 1; $i <= 5; $i++) {
+ $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0];
+ $this->drupalPostForm(NULL, $edit, 'Claim the next item and delete it');
+ $this->assertPattern(t('%Claimed and deleted item id=.*string=@string for 0 seconds.%', ['@string' => 'boogie' . $i]));
+ }
+ // Verify that nothing is left to claim.
+ $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0];
+ $this->drupalPostForm(NULL, $edit, 'Claim the next item from the queue');
+ $this->assertText('There were no items in the queue available to claim');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/simpletest_example/config/install/core.entity_form_display.node.simpletest_example.default.yml b/sites/all/modules/examples/examples/simpletest_example/config/install/core.entity_form_display.node.simpletest_example.default.yml
new file mode 100644
index 000000000..959c8c93b
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/config/install/core.entity_form_display.node.simpletest_example.default.yml
@@ -0,0 +1,64 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.node.simpletest_example.body
+ - node.type.simpletest_example
+ module:
+ - path
+ - text
+ enforced:
+ module:
+ - simpletest_example
+id: node.simpletest_example.default
+targetEntityType: node
+bundle: simpletest_example
+mode: default
+content:
+ title:
+ type: string_textfield
+ weight: -5
+ settings:
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ uid:
+ type: entity_reference_autocomplete
+ weight: 5
+ settings:
+ match_operator: CONTAINS
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ created:
+ type: datetime_timestamp
+ weight: 10
+ settings: { }
+ third_party_settings: { }
+ promote:
+ type: boolean_checkbox
+ weight: 15
+ settings:
+ display_label: true
+ third_party_settings: { }
+ sticky:
+ type: boolean_checkbox
+ weight: 16
+ settings:
+ display_label: true
+ third_party_settings: { }
+ path:
+ type: path
+ weight: 30
+ settings: { }
+ third_party_settings: { }
+ body:
+ type: text_textarea_with_summary
+ weight: 31
+ settings:
+ rows: 9
+ summary_rows: 3
+ placeholder: ''
+ third_party_settings: { }
+hidden: { }
+third_party_settings: { }
diff --git a/sites/all/modules/examples/examples/simpletest_example/config/install/field.field.node.simpletest_example.body.yml b/sites/all/modules/examples/examples/simpletest_example/config/install/field.field.node.simpletest_example.body.yml
new file mode 100644
index 000000000..6b5df1a90
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/config/install/field.field.node.simpletest_example.body.yml
@@ -0,0 +1,25 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.node.body
+ - node.type.simpletest_example
+ module:
+ - text
+ enforced:
+ module:
+ - simpletest_example
+id: node.simpletest_example.body
+field_name: body
+entity_type: node
+bundle: simpletest_example
+label: Body
+description: ''
+required: false
+translatable: true
+default_value: { }
+default_value_callback: ''
+settings:
+ display_summary: false
+third_party_settings: { }
+field_type: text_with_summary
diff --git a/sites/all/modules/examples/examples/simpletest_example/config/install/node.type.simpletest_example.yml b/sites/all/modules/examples/examples/simpletest_example/config/install/node.type.simpletest_example.yml
new file mode 100644
index 000000000..06af9fcb6
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/config/install/node.type.simpletest_example.yml
@@ -0,0 +1,14 @@
+langcode: en
+status: true
+dependencies:
+ enforced:
+ module:
+ - simpletest_example
+name: 'SimpleTest Example Node Type'
+type: simpletest_example
+description: 'A content type that exists so we can test it.'
+help: ''
+new_revision: false
+preview_mode: 1
+display_submitted: true
+third_party_settings: { }
diff --git a/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.info.yml b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.info.yml
new file mode 100644
index 000000000..75add8f0f
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.info.yml
@@ -0,0 +1,14 @@
+name: "SimpleTest Example Mock Module"
+type: module
+hidden: true
+description: "Mock module for the SimpleTest Example module."
+package: Example modules
+# core: 8.x
+dependencies:
+ - simpletest_example
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.module b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.module
new file mode 100644
index 000000000..79974f615
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.module
@@ -0,0 +1,38 @@
+getType() == 'simpletest_example') {
+ $build['simpletest_example_test_section'] = [
+ '#markup' => t('The test module did its thing.'),
+ '#weight' => -99,
+ ];
+ }
+}
diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.info.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.info.yml
new file mode 100644
index 000000000..8965094f6
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.info.yml
@@ -0,0 +1,22 @@
+name: SimpleTest Example
+type: module
+hidden: false
+description: 'Demonstrates some SimpleTest-based tests in Drupal 8.'
+package: Example modules
+# core: 8.x
+# We have to be as explicit as possible about the dependencies for this module.
+# If you look at the config info in the config/install/ directory, you'll see
+# that they depend on some of these modules.
+dependencies:
+ - drupal:simpletest
+ - drupal:node
+ - drupal:field
+ - drupal:path
+ - drupal:text
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.links.menu.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.links.menu.yml
new file mode 100644
index 000000000..639a8cbc6
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.links.menu.yml
@@ -0,0 +1,3 @@
+simpletest_example.description:
+ title: SimpleTest Example
+ route_name: simpletest_example_description
diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.module b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.module
new file mode 100644
index 000000000..62548bf8e
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.module
@@ -0,0 +1,60 @@
+getType();
+ // If it's not a simpletest_example node, or if it's not operations we care
+ // about, then just ignore.
+ if ($type != 'simpletest_example' || ($op != 'update' && $op != 'delete')) {
+ return AccessResult::neutral();
+ }
+
+ // This code has a BUG that we'll find in testing.
+ //
+ // This is the incorrect version we'll use to demonstrate test failure.
+ // The correct version should have ($op == 'update' || $op == 'delete').
+ // The author had mistakenly always tested with User 1 so it always
+ // allowed access and the bug wasn't noticed!
+ if (($op == 'delete') && ($account->hasPermission('extra special edit any simpletest_example') && ($account->id() == $node->getAuthorId()))) {
+ return AccessResult::allowed();
+ }
+
+ return AccessResult::forbidden();
+}
+
+/**
+ * @} End of "defgroup simpletest_example".
+ */
diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.permissions.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.permissions.yml
new file mode 100644
index 000000000..b250a73e3
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.permissions.yml
@@ -0,0 +1,6 @@
+# In this case we're adding an addition permission that does the same
+# as the one the node module offers, just to demonstrate this error.
+'extra special edit any simpletest_example':
+ title: Extra special edit any SimpleTest Example
+ description: Allow user to edit any SimpleTest Example content authored by any user.
+ 'restrict access': TRUE
diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.routing.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.routing.yml
new file mode 100644
index 000000000..2bf0834ab
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.routing.yml
@@ -0,0 +1,8 @@
+# This module only has one route.
+# It is to a page explaining the module.
+simpletest_example_description:
+ path: 'examples/simpletest-example'
+ defaults:
+ _controller: '\Drupal\simpletest_example\Controller\SimpleTestExampleController::description'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/simpletest_example/src/Controller/SimpleTestExampleController.php b/sites/all/modules/examples/examples/simpletest_example/src/Controller/SimpleTestExampleController.php
new file mode 100644
index 000000000..1277e1f89
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/src/Controller/SimpleTestExampleController.php
@@ -0,0 +1,21 @@
+drupalCreateUser(['access content']);
+ // Log them in.
+ $this->drupalLogin($test_user);
+ // Set up some content.
+ $settings = [
+ 'type' => 'simpletest_example',
+ 'title' => $this->randomMachineName(32),
+ ];
+ // Create the content node.
+ $node = $this->drupalCreateNode($settings);
+ // View the node.
+ $this->drupalGet('node/' . $node->id());
+ // Check that our module did it's thing.
+ $this->assertText(t('The test module did its thing.'), "Found evidence of test module.");
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleTest.php b/sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleTest.php
new file mode 100644
index 000000000..abf8cd0e4
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleTest.php
@@ -0,0 +1,168 @@
+drupalGet('');
+ $this->assertResponse(200, 'The Home page is available.');
+ $this->assertLinkByHref('examples/simpletest-example');
+
+ // Verify that anonymous can access the simpletest_examples page.
+ $this->drupalGet('examples/simpletest-example');
+ $this->assertResponse(200, 'The SimpleTest Example description page is available.');
+ }
+
+ /**
+ * Test node creation through the user interface.
+ *
+ * Creates a node using the node/add form and verifies its consistency in
+ * the database.
+ */
+ public function testSimpleTestExampleCreate() {
+ // Create a user with the ability to create our content type. This
+ // permission is generated by the node module.
+ $user = $this->createUser(['create simpletest_example content']);
+ // Log in our user.
+ $this->drupalLogin($user);
+
+ // Create a node using the node/add form.
+ $edit = [];
+ $edit['title[0][value]'] = $this->randomMachineName(8);
+ $edit['body[0][value]'] = $this->randomMachineName(16);
+ $this->drupalPostForm('node/add/simpletest_example', $edit, 'Save');
+
+ // Check that our simpletest_example node has been created.
+ $this->assertText(t('@post @title has been created.', [
+ '@post' => 'SimpleTest Example Node Type',
+ '@title' => $edit['title[0][value]'],
+ ]));
+ // Check that the node exists in the database.
+ $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
+ $this->assertTrue($node, 'Node found in database.');
+
+ // Verify 'submitted by' information. Drupal adds a newline in there, so
+ // we have to check for that.
+ $username = $this->loggedInUser->getUsername();
+ $datetime = format_date($node->getCreatedTime());
+ $submitted_by = "Submitted by $username\n on $datetime";
+
+ $this->drupalGet('node/' . $node->id());
+ $this->assertText($submitted_by);
+ }
+
+ /**
+ * Create a simpletest_example node and then see if our user can edit it.
+ *
+ * Note that some assertions in this test will fail. We do this to show what
+ * a failing test looks like. Since we don't want this to interfere with
+ * automated tests, however, we jump through some hoops to determine our
+ * environment.
+ */
+ public function testSimpleTestExampleEdit() {
+ // Create a user with our special permission.
+ $user = $this->drupalCreateUser(['extra special edit any simpletest_example']);
+ // Log in our user.
+ $this->drupalLogin($user);
+
+ // Create a node with our user as the creator.
+ // drupalCreateNode() uses the logged-in user by default.
+ $settings = [
+ 'type' => 'simpletest_example',
+ 'title' => $this->randomMachineName(32),
+ ];
+ $node = $this->drupalCreateNode($settings);
+
+ // For debugging, we might output some information using $this->verbose()
+ // It will only be output if the testing settings have 'verbose' set.
+ $this->verbose('Node created: ' . $node->getTitle());
+
+ // This section demonstrates a failing test. However, we want this test to
+ // pass when it's running on the Drupal QA testbot. So we need to determine
+ // which environment we're running inside of before we continue.
+ if (!$this->runningOnTestbot()) {
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ // The debug() statement will output information into the test results.
+ // It can also be used in Drupal anywhere in code and will come out
+ // as a drupal_set_message().
+ debug('The following test should fail. Examine the verbose message above it to see why.');
+ // Make sure we don't get a 401 unauthorized response:
+ $this->assertResponse(200, 'User is allowed to edit the content.');
+
+ // Looking for title text in the page to determine whether we were
+ // successful opening edit form.
+ $this->assertText(t("@title", ['@title' => $settings['title']]), "Found title in edit form");
+ }
+ }
+
+ /**
+ * Detect if we're running on PIFR testbot.
+ *
+ * We can skip intentional failure if we're on the testbot. It happens that
+ * on the testbot the site under test is in a directory named 'checkout' or
+ * 'site_under_test'.
+ *
+ * @return bool
+ * TRUE if running on testbot.
+ */
+ public function runningOnTestbot() {
+ // @todo: Add this line back once the testbot variable is available.
+ // https://www.drupal.org/node/2565181
+ // return env('DRUPALCI');
+ return TRUE;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/simpletest_example/templates/description.html.twig b/sites/all/modules/examples/examples/simpletest_example/templates/description.html.twig
new file mode 100644
index 000000000..e47240d4e
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/templates/description.html.twig
@@ -0,0 +1,18 @@
+{#
+
+Description text for the Simpletest Example.
+
+#}
+
+{% trans %}
+
Please note that the use of SimpleTest is deprecated. This example module will
+be removed in Drupal 9, and new tests should not be written using SimpleTest. In
+addition, all existing SimpleTest tests should be converted to PHPUnit
+functional tests.
+
+
+There are some instructions for how to convert Simpletest-based tests to the new
+BrowserTestBase in this change notice:
+https://www.drupal.org/node/2469723.
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/simpletest_example/tests/src/Functional/SimpletestExampeTest.php b/sites/all/modules/examples/examples/simpletest_example/tests/src/Functional/SimpletestExampeTest.php
new file mode 100644
index 000000000..edff10d9c
--- /dev/null
+++ b/sites/all/modules/examples/examples/simpletest_example/tests/src/Functional/SimpletestExampeTest.php
@@ -0,0 +1,48 @@
+assertSession();
+
+ // The simpletest_example module should have been installed by the test, so
+ // we can just uninstall it.
+ /* @var $module_installer \Drupal\Core\Extension\ModuleInstallerInterface */
+ $module_installer = $this->container->get('module_installer');
+ $module_installer->uninstall(['simpletest_example']);
+ $this->drupalGet('examples/simpletest-example');
+ $session->statusCodeEquals(404);
+
+ // We reinstall the simpletest_example module to make sure it happens
+ // properly.
+ $module_installer->install(['simpletest_example']);
+ $this->drupalGet('examples/simpletest-example');
+ $session->statusCodeEquals(200);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/src/Tests/ExamplesTest.php b/sites/all/modules/examples/examples/src/Tests/ExamplesTest.php
new file mode 100644
index 000000000..e55717a76
--- /dev/null
+++ b/sites/all/modules/examples/examples/src/Tests/ExamplesTest.php
@@ -0,0 +1,73 @@
+drupalCreateUser(['access toolbar']);
+ $this->drupalLogin($user);
+
+ // Check for the 'Examples' tab.
+ $this->drupalGet('');
+
+ // Assert that the toolbar tab registered by examples is present.
+ $this->assertLink('Examples');
+
+ // Assert that the toolbar tab registered by examples is present.
+ $this->assertEqual(
+ \count($this->xpath('//nav/div/a[@data-toolbar-tray="toolbar-item-examples-tray"]')),
+ 1,
+ 'Found the Examples toolbar tab.'
+ );
+
+ // Assert that the toolbar tray registered by examples is present.
+ $this->assertEqual(
+ \count($this->xpath('//nav/div/div[@data-toolbar-tray="toolbar-item-examples-tray"]')),
+ 1,
+ 'Found the Examples toolbar tray.'
+ );
+ // Assert that PHPUnit link does not appears in the tray.
+ $phpunit_link = 'PHPUnit Example';
+ $this->assertNoLink($phpunit_link);
+ $this->assertNoRaw('
');
+
+ // Install phpunit_example and see if it appears in the toolbar. We use
+ // phpunit_example because it's very light-weight.
+ $this->container->get('module_installer')->install(['phpunit_example'], TRUE);
+ // SimpleTest needs for us to reset all the caches.
+ $this->resetAll();
+
+ // Verify that PHPUnit appears in the tray.
+ $this->drupalGet('');
+ $this->assertLink($phpunit_link);
+ // Assert that the PHPUnit tray item is present.
+ $this->assertEqual(
+ \count($this->xpath('//nav/div/div/nav/ul/li[@class="phpunit-example"]')),
+ 1,
+ 'Found the PHPUnit Example tray item.'
+ );
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/src/Tests/ExamplesTestBase.php b/sites/all/modules/examples/examples/src/Tests/ExamplesTestBase.php
new file mode 100644
index 000000000..e920b5910
--- /dev/null
+++ b/sites/all/modules/examples/examples/src/Tests/ExamplesTestBase.php
@@ -0,0 +1,41 @@
+setupExamplesMenus();
+ }
+
+ /**
+ * Set up menus and tasks in their regions.
+ *
+ * Since menus and tasks are now blocks, we're required to explicitly set them
+ * to regions. This method standardizes the way we do that for Examples.
+ *
+ * Note that subclasses must explicitly declare that the block module is a
+ * dependency.
+ */
+ protected function setupExamplesMenus() {
+ $this->drupalPlaceBlock('system_menu_block:tools', ['region' => 'primary_menu']);
+ $this->drupalPlaceBlock('local_tasks_block', ['region' => 'secondary_menu']);
+ $this->drupalPlaceBlock('local_actions_block', ['region' => 'content']);
+ $this->drupalPlaceBlock('page_title_block', ['region' => 'content']);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/src/Utility/DescriptionTemplateTrait.php b/sites/all/modules/examples/examples/src/Utility/DescriptionTemplateTrait.php
new file mode 100644
index 000000000..3498b25cb
--- /dev/null
+++ b/sites/all/modules/examples/examples/src/Utility/DescriptionTemplateTrait.php
@@ -0,0 +1,78 @@
+getDescriptionTemplatePath();
+ $template = file_get_contents($template_path);
+ $build = [
+ 'description' => [
+ '#type' => 'inline_template',
+ '#template' => $template,
+ '#context' => $this->getDescriptionVariables(),
+ ],
+ ];
+ return $build;
+ }
+
+ /**
+ * Name of our module.
+ *
+ * @return string
+ * A module name.
+ */
+ abstract protected function getModuleName();
+
+ /**
+ * Variables to act as context to the twig template file.
+ *
+ * @return array
+ * Associative array that defines context for a template.
+ */
+ protected function getDescriptionVariables() {
+ $variables = [
+ 'module' => $this->getModuleName(),
+ ];
+ return $variables;
+ }
+
+ /**
+ * Get full path to the template.
+ *
+ * @return string
+ * Path string.
+ */
+ protected function getDescriptionTemplatePath() {
+ return drupal_get_path('module', $this->getModuleName()) . "/templates/description.html.twig";
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/Controller/StreamWrapperExampleController.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/Controller/StreamWrapperExampleController.php
new file mode 100644
index 000000000..7d607a054
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/Controller/StreamWrapperExampleController.php
@@ -0,0 +1,24 @@
+ [
+ '#theme' => 'example_description',
+ ],
+ ];
+ return $build;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php
new file mode 100644
index 000000000..dd5836a33
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php
@@ -0,0 +1,30 @@
+query->has('file')) {
+ $file_path = preg_replace('|^\/examples\/stream_wrapper_example\/files\/|', '', $path);
+ $request->query->set('file', $file_path);
+ // We return the route we want to match.
+ return '/examples/stream_wrapper_example/files';
+ }
+ return $path;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php
new file mode 100644
index 000000000..eaed8aa29
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php
@@ -0,0 +1,879 @@
+requestStack = \Drupal::service('request_stack');
+ $helper = $this->getSessionWrapper();
+ $helper->setPath('.isadir.txt', TRUE);
+ $this->streamMode = FALSE;
+ }
+
+ /**
+ * Get wrapped session manipulators.
+ */
+ public function getSessionWrapper() {
+ return new SessionWrapper($this->requestStack);
+ }
+
+ /**
+ * Returns the name of the stream wrapper for use in the UI.
+ *
+ * @return string
+ * The stream wrapper name.
+ */
+ public function getName() {
+ return t('File Example Session files');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return t('Simulated file system using your session storage. Not for real use!');
+ }
+
+ /**
+ * Implements setUri().
+ */
+ public function setUri($uri) {
+ $this->uri = $uri;
+ }
+
+ /**
+ * Implements getUri().
+ */
+ public function getUri() {
+ return $this->uri;
+ }
+
+ /**
+ * Implements getTarget().
+ *
+ * The "target" is the portion of the URI to the right of the scheme.
+ * So in session://example/test.txt, the target is 'example/test.txt'.
+ *
+ * @todo Figure out what this is in the new API.
+ */
+ public function getTarget($uri = NULL) {
+ if (!isset($uri)) {
+ $uri = $this->uri;
+ }
+
+ list($scheme, $target) = explode('://', $uri, 2);
+
+ // Remove erroneous leading or trailing, forward-slashes and backslashes.
+ // In the session:// scheme, there is never a leading slash on the target.
+ return trim($target, '\/');
+ }
+
+ /**
+ * Implements getDirectoryPath().
+ *
+ * In this case there is no directory string, so return an empty string.
+ */
+ public function getDirectoryPath() {
+ return '';
+ }
+
+ /**
+ * Overrides getExternalUrl().
+ *
+ * We have set up a helper function and menu entry to provide access to this
+ * key via HTTP; normally it would be accessible some other way.
+ */
+ public function getExternalUrl() {
+ $path = str_replace('\\', '/', $this->getTarget());
+ return $this->url('stream_wrapper_example.files.session', ['filepath' => $path, 'scheme' => 'session'], ['absolute' => TRUE]);
+ }
+
+ /**
+ * Returns canonical, absolute path of the resource.
+ *
+ * Implementation placeholder. PHP's realpath() does not support stream
+ * wrappers. We provide this as a default so that individual wrappers may
+ * implement their own solutions.
+ *
+ * @return string
+ * Returns a string with absolute pathname on success (implemented
+ * by core wrappers), or FALSE on failure or if the registered
+ * wrapper does not provide an implementation.
+ */
+ public function realpath() {
+ return 'session://' . $this->getLocalPath();
+ }
+
+ /**
+ * Returns the local path.
+ *
+ * Here we aren't doing anything but stashing the "file" in a key in the
+ * $_SESSION variable, so there's not much to do but to create a "path"
+ * which is really just a key in the $_SESSION variable. So something
+ * like 'session://one/two/three.txt' becomes
+ * $_SESSION['stream_wrapper_example']['one']['two']['three.txt'] and the
+ * actual path is "one/two/three.txt".
+ *
+ * @param string $uri
+ * Optional URI, supplied when doing a move or rename.
+ */
+ protected function getLocalPath($uri = NULL) {
+ if (!isset($uri)) {
+ $uri = $this->uri;
+ }
+
+ $path = str_replace('session://', '', $uri);
+ $path = trim($path, '/');
+ return $path;
+ }
+
+ /**
+ * Opens a stream, as for fopen(), file_get_contents(), file_put_contents().
+ *
+ * @param string $uri
+ * A string containing the URI to the file to open.
+ * @param string $mode
+ * The file mode ("r", "wb" etc.).
+ * @param int $options
+ * A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
+ * @param string &$opened_path
+ * A string containing the path actually opened.
+ *
+ * @return bool
+ * Returns TRUE if file was opened successfully. (Always returns TRUE).
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-open.php
+ */
+ public function stream_open($uri, $mode, $options, &$opened_path) {
+ $this->uri = $uri;
+ $path = $this->getLocalPath($uri);
+ // We will support two modes only, 'r' and 'w'. If the key is 'r',
+ // check to make sure the file is there.
+ if (stristr($mode, 'r') !== FALSE) {
+ $helper = $this->getSessionWrapper();
+ if (!$helper->checkPath($path)) {
+ return FALSE;
+ }
+ else {
+ $buffer = $helper->getPath($path);
+ if (!is_string($buffer)) {
+ return FALSE;
+ }
+ $this->sessionContent = $buffer;
+ }
+ $this->streamMode = 'r';
+ }
+ else {
+ $this->sessionContent = '';
+ $this->streamMode = 'w';
+ }
+ // Reset the stream pointer since this is an open.
+ $this->streamPointer = 0;
+ return TRUE;
+ }
+
+ /**
+ * Retrieve the underlying stream resource.
+ *
+ * This method is called in response to stream_select().
+ *
+ * @param int $cast_as
+ * Can be STREAM_CAST_FOR_SELECT when stream_select() is calling
+ * stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for
+ * other uses.
+ *
+ * @return resource|false
+ * The underlying stream resource or FALSE if stream_select() is not
+ * supported.
+ *
+ * @see stream_select()
+ * @see http://php.net/manual/streamwrapper.stream-cast.php
+ */
+ public function stream_cast($cast_as) {
+ return FALSE;
+ }
+
+ /**
+ * Sets metadata on the stream.
+ *
+ * @param string $path
+ * A string containing the URI to the file to set metadata on.
+ * @param int $option
+ * One of:
+ * - STREAM_META_TOUCH: The method was called in response to touch().
+ * - STREAM_META_OWNER_NAME: The method was called in response to chown()
+ * with string parameter.
+ * - STREAM_META_OWNER: The method was called in response to chown().
+ * - STREAM_META_GROUP_NAME: The method was called in response to chgrp().
+ * - STREAM_META_GROUP: The method was called in response to chgrp().
+ * - STREAM_META_ACCESS: The method was called in response to chmod().
+ * @param mixed $value
+ * If option is:
+ * - STREAM_META_TOUCH: Array consisting of two arguments of the touch()
+ * function.
+ * - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner
+ * user/group as string.
+ * - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner
+ * user/group as integer.
+ * - STREAM_META_ACCESS: The argument of the chmod() as integer.
+ *
+ * @return bool
+ * Returns TRUE on success or FALSE on failure. If $option is not
+ * implemented, FALSE should be returned.
+ *
+ * @see http://www.php.net/manual/streamwrapper.stream-metadata.php
+ */
+ public function stream_metadata($path, $option, $value) {
+ // We don't really do any of these, but we want to reassure the calling code
+ // that there is no problem with chown or chgrp, even though we do not
+ // actually support these.
+ return TRUE;
+ }
+
+ /**
+ * Change stream options.
+ *
+ * This method is called to set options on the stream.
+ *
+ * @param int $option
+ * One of:
+ * - STREAM_OPTION_BLOCKING: The method was called in response to
+ * stream_set_blocking().
+ * - STREAM_OPTION_READ_TIMEOUT: The method was called in response to
+ * stream_set_timeout().
+ * - STREAM_OPTION_WRITE_BUFFER: The method was called in response to
+ * stream_set_write_buffer().
+ * @param int $arg1
+ * If option is:
+ * - STREAM_OPTION_BLOCKING: The requested blocking mode:
+ * - 1 means blocking.
+ * - 0 means not blocking.
+ * - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds.
+ * - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or
+ * STREAM_BUFFER_FULL.
+ * @param int $arg2
+ * If option is:
+ * - STREAM_OPTION_BLOCKING: This option is not set.
+ * - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds.
+ * - STREAM_OPTION_WRITE_BUFFER: The requested buffer size.
+ *
+ * @return bool
+ * TRUE on success, FALSE otherwise. If $option is not implemented, FALSE
+ * should be returned.
+ */
+ public function stream_set_option($option, $arg1, $arg2) {
+ return FALSE;
+ }
+
+ /**
+ * Truncate stream.
+ *
+ * Will respond to truncation; e.g., through ftruncate().
+ *
+ * @param int $new_size
+ * The new size.
+ *
+ * @return bool
+ * TRUE on success, FALSE otherwise.
+ *
+ * @todo
+ * This one actually makes sense for the example.
+ */
+ public function stream_truncate($new_size) {
+ return FALSE;
+ }
+
+ /**
+ * Support for flock().
+ *
+ * The $_SESSION variable has no locking capability, so return TRUE.
+ *
+ * @param int $operation
+ * One of the following:
+ * - LOCK_SH to acquire a shared lock (reader).
+ * - LOCK_EX to acquire an exclusive lock (writer).
+ * - LOCK_UN to release a lock (shared or exclusive).
+ * - LOCK_NB if you don't want flock() to block while locking (not
+ * supported on Windows).
+ *
+ * @return bool
+ * Always returns TRUE at the present time. (no support)
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-lock.php
+ */
+ public function stream_lock($operation) {
+ return TRUE;
+ }
+
+ /**
+ * Support for fread(), file_get_contents() etc.
+ *
+ * @param int $count
+ * Maximum number of bytes to be read.
+ *
+ * @return string
+ * The string that was read, or FALSE in case of an error.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-read.php
+ */
+ public function stream_read($count) {
+ if (is_string($this->sessionContent)) {
+ $remaining_chars = strlen($this->sessionContent) - $this->streamPointer;
+ $number_to_read = min($count, $remaining_chars);
+ if ($remaining_chars > 0) {
+ $buffer = substr($this->sessionContent, $this->streamPointer, $number_to_read);
+ $this->streamPointer += $number_to_read;
+ return $buffer;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for fwrite(), file_put_contents() etc.
+ *
+ * @param string $data
+ * The string to be written.
+ *
+ * @return int
+ * The number of bytes written (integer).
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-write.php
+ */
+ public function stream_write($data) {
+ // Sanitize the data in a simple way since we're putting it into the
+ // session variable.
+ $data = Html::escape($data);
+ $this->sessionContent = substr_replace($this->sessionContent, $data, $this->streamPointer);
+ $this->streamPointer += strlen($data);
+ return strlen($data);
+ }
+
+ /**
+ * Support for feof().
+ *
+ * @return bool
+ * TRUE if end-of-file has been reached.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-eof.php
+ */
+ public function stream_eof() {
+ return FALSE;
+ }
+
+ /**
+ * Support for fseek().
+ *
+ * @param int $offset
+ * The byte offset to got to.
+ * @param int $whence
+ * SEEK_SET, SEEK_CUR, or SEEK_END.
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-seek.php
+ */
+ public function stream_seek($offset, $whence = SEEK_SET) {
+ if (strlen($this->sessionContent) >= $offset) {
+ $this->streamPointer = $offset;
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for fflush().
+ *
+ * @return bool
+ * TRUE if data was successfully stored (or there was no data to store).
+ * This always returns TRUE, as this example provides and needs no
+ * flush support.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-flush.php
+ */
+ public function stream_flush() {
+ if ($this->streamMode == 'w') {
+ // Since we aren't writing directly to the session, we need to send
+ // the bytes on to the store.
+ $helper = $this->getSessionWrapper();
+ $path = $this->getLocalPath($this->uri);
+ $helper->setPath($path, $this->sessionContent);
+ $this->sessionContent = '';
+ $this->streamPointer = 0;
+ }
+ return TRUE;
+ }
+
+ /**
+ * Support for ftell().
+ *
+ * @return int
+ * The current offset in bytes from the beginning of file.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-tell.php
+ */
+ public function stream_tell() {
+ return $this->streamPointer;
+ }
+
+ /**
+ * Support for fstat().
+ *
+ * @return array
+ * An array with file status, or FALSE in case of an error - see fstat()
+ * for a description of this array.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-stat.php
+ */
+ public function stream_stat() {
+ return [
+ 'size' => strlen($this->sessionContent),
+ ];
+ }
+
+ /**
+ * Support for fclose().
+ *
+ * @return bool
+ * TRUE if stream was successfully closed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.stream-close.php
+ */
+ public function stream_close() {
+ $this->streamPointer = 0;
+ // Unassign the reference.
+ unset($this->sessionContent);
+ return TRUE;
+ }
+
+ /**
+ * Support for unlink().
+ *
+ * @param string $uri
+ * A string containing the uri to the resource to delete.
+ *
+ * @return bool
+ * TRUE if resource was successfully deleted.
+ *
+ * @see http://php.net/manual/en/streamwrapper.unlink.php
+ */
+ public function unlink($uri) {
+ $path = $this->getLocalPath($uri);
+ $helper = $this->getSessionWrapper();
+ $helper->clearPath($path);
+ return TRUE;
+ }
+
+ /**
+ * Support for rename().
+ *
+ * @param string $from_uri
+ * The uri to the file to rename.
+ * @param string $to_uri
+ * The new uri for file.
+ *
+ * @return bool
+ * TRUE if file was successfully renamed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.rename.php
+ */
+ public function rename($from_uri, $to_uri) {
+ // We get the old key contents, write it
+ // to a new key, erase the old key.
+ $from_path = $this->getLocalPath($from_uri);
+ $to_path = $this->getLocalPath($to_uri);
+ $helper = $this->getSessionWrapper();
+ if (!$helper->checkPath($from_path)) {
+ return FALSE;
+ }
+ $from_key = $helper->getPath($from_path);
+ $path_info = $helper->getParentPath($to_path);
+ $parent_path = $path_info['dirname'];
+ $new_file = $path_info['basename'];
+ // We will only allow writing to a non-existent file
+ // in an existing directory.
+ if ($helper->checkPath($parent_path) && !$helper->checkPath($to_path)) {
+ $helper->setPath($to_path, $from_key);
+ $helper->clearPath($from_path);
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Gets the name of the directory from a given path.
+ *
+ * @param string $uri
+ * A URI.
+ *
+ * @return string
+ * A string containing the directory name.
+ *
+ * @see drupal_dirname()
+ */
+ public function dirname($uri = NULL) {
+ list($scheme, $target) = explode('://', $uri, 2);
+ $target = $this->getTarget($uri);
+ if (strpos($target, '/')) {
+ $dirname = preg_replace('@/[^/]*$@', '', $target);
+ }
+ else {
+ $dirname = '';
+ }
+ return $scheme . '://' . $dirname;
+ }
+
+ /**
+ * Support for mkdir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to create.
+ * @param int $mode
+ * Permission flags - see mkdir().
+ * @param int $options
+ * A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
+ *
+ * @return bool
+ * TRUE if directory was successfully created.
+ *
+ * @see http://php.net/manual/en/streamwrapper.mkdir.php
+ */
+ public function mkdir($uri, $mode, $options) {
+ // If this already exists, then we can't mkdir.
+ if (is_dir($uri) || is_file($uri)) {
+ return FALSE;
+ }
+ $path = $this->getLocalPath($uri);
+ $helper = $this->getSessionWrapper();
+ $new_dir = ['isadir.txt' => TRUE];
+ $helper->setPath($path, $new_dir);
+ return TRUE;
+ }
+
+ /**
+ * Support for rmdir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to delete.
+ * @param int $options
+ * A bit mask of STREAM_REPORT_ERRORS.
+ *
+ * @return bool
+ * TRUE if directory was successfully removed.
+ *
+ * @see http://php.net/manual/en/streamwrapper.rmdir.php
+ */
+ public function rmdir($uri, $options) {
+ $path = $this->getLocalPath($uri);
+ $helper = $this->getSessionWrapper();
+ if (!$helper->checkPath($path) or !is_array($helper->getPath($path))) {
+ return FALSE;
+ }
+ $helper->clearPath($path);
+ return TRUE;
+ }
+
+ /**
+ * Support for stat().
+ *
+ * This important function goes back to the Unix way of doing things.
+ * In this example almost the entire stat array is irrelevant, but the
+ * mode is very important. It tells PHP whether we have a file or a
+ * directory and what the permissions are. All that is packed up in a
+ * bitmask. This is not normal PHP fodder.
+ *
+ * @param string $uri
+ * A string containing the URI to get information about.
+ * @param int $flags
+ * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
+ *
+ * @return array|bool
+ * An array with file status, or FALSE in case of an error - see fstat()
+ * for a description of this array.
+ *
+ * @see http://php.net/manual/en/streamwrapper.url-stat.php
+ */
+ public function url_stat($uri, $flags) {
+ $path = $this->getLocalPath($uri);
+ $helper = $this->getSessionWrapper();
+ if (!$helper->checkPath($path)) {
+ return FALSE;
+ // No file.
+ }
+ // Default to fail.
+ $return = FALSE;
+ $mode = 0;
+
+ $path_info = $helper->getParentPath($path);
+ $key = $helper->getPath($path);
+ $key_name = $path_info['basename'];
+ // We will call an array a directory and the root is always an array.
+ if (is_array($key)) {
+ // S_IFDIR means it's a directory.
+ $mode = 0040000;
+ }
+ elseif ($key !== FALSE) {
+ // S_IFREG, means it's a file.
+ $mode = 0100000;
+ }
+
+ if ($mode) {
+ $size = 0;
+ if ($mode == 0100000) {
+ $size = strlen($key);
+ }
+
+ // There are no protections on this, so all writable.
+ $mode |= 0777;
+ $return = [
+ 'dev' => 0,
+ 'ino' => 0,
+ 'mode' => $mode,
+ 'nlink' => 0,
+ 'uid' => 0,
+ 'gid' => 0,
+ 'rdev' => 0,
+ 'size' => $size,
+ 'atime' => 0,
+ 'mtime' => 0,
+ 'ctime' => 0,
+ 'blksize' => 0,
+ 'blocks' => 0,
+ ];
+ }
+ return $return;
+ }
+
+ /**
+ * Support for opendir().
+ *
+ * @param string $uri
+ * A string containing the URI to the directory to open.
+ * @param int $options
+ * Whether or not to enforce safe_mode (0x04).
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-opendir.php
+ */
+ public function dir_opendir($uri, $options) {
+ $path = $this->getLocalPath($uri);
+ $helper = $this->getSessionWrapper();
+ if (!$helper->checkPath($path)) {
+ return FALSE;
+ }
+ $var = $helper->getPath($path);
+ if (!is_array($var)) {
+ return FALSE;
+ }
+
+ // We grab the list of key names, flip it so that .isadir.txt can easily
+ // be removed, then flip it back so we can easily walk it as a list.
+ $this->directoryKeys = array_flip(array_keys($var));
+ unset($this->directoryKeys['.isadir.txt']);
+ $this->directoryKeys = array_keys($this->directoryKeys);
+ $this->directoryPointer = 0;
+ return TRUE;
+ }
+
+ /**
+ * Support for readdir().
+ *
+ * @return string|bool
+ * The next filename, or FALSE if there are no more files in the directory.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-readdir.php
+ */
+ public function dir_readdir() {
+ if ($this->directoryPointer < count($this->directoryKeys)) {
+ $next = $this->directoryKeys[$this->directoryPointer];
+ $this->directoryPointer++;
+ return $next;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Support for rewinddir().
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php
+ */
+ public function dir_rewinddir() {
+ $this->directoryPointer = 0;
+ return TRUE;
+ }
+
+ /**
+ * Support for closedir().
+ *
+ * @return bool
+ * TRUE on success.
+ *
+ * @see http://php.net/manual/en/streamwrapper.dir-closedir.php
+ */
+ public function dir_closedir() {
+ $this->directoryPointer = 0;
+ unset($this->directoryKeys);
+ return TRUE;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php
new file mode 100644
index 000000000..ba59b0d49
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php
@@ -0,0 +1,100 @@
+sessionStore = [];
+ $session = $this->prophesize(SessionInterface::class);
+ $test = $this;
+
+ $session
+ ->get('stream_wrapper_example', [])
+ ->will(function ($args) use ($test) {
+ return $test->getSessionStore();
+ });
+
+ $session
+ ->set('stream_wrapper_example', Argument::any())
+ ->will(function ($args) use ($test) {
+ $test->setSessionStore($args[1]);
+ });
+
+ $session
+ ->remove('stream_wrapper_example')
+ ->will(function ($args) use ($test) {
+ $test->resetSessionStore();
+ });
+
+ $request = $this->prophesize(Request::class);
+ $request
+ ->getSession()
+ ->willReturn($session->reveal());
+
+ $request_stack = $this->prophesize(RequestStack::class);
+ $request_stack
+ ->getCurrentRequest()
+ ->willReturn($request->reveal());
+
+ return $this->requestStack = $request_stack->reveal();
+ }
+
+ /**
+ * Get a session wrapper.
+ */
+ public function getSessionWrapper() {
+ return new SessionWrapper($this->requestStack);
+ }
+
+ /**
+ * Helper for mocks.
+ */
+ public function getSessionStore() {
+ return $this->sessionStore;
+ }
+
+ /**
+ * Helper for our mocks.
+ */
+ public function setSessionStore($data) {
+ $this->sessionStore = $data;
+ }
+
+ /**
+ * Helper for our mocks.
+ */
+ public function resetSessionStore() {
+ $this->sessionStore = [];
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php
new file mode 100644
index 000000000..a540a5670
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php
@@ -0,0 +1,247 @@
+requestStack = $request_stack;
+ $this->storePath = '';
+ }
+
+ /**
+ * Get a fresh session object.
+ *
+ * @return \Symfony\Component\HttpFoundation\Session\SessionInterface
+ * A session object.
+ */
+ protected function getSession() {
+ return $this->requestStack->getCurrentRequest()->getSession();
+ }
+
+ /**
+ * Get whatever's in the store.
+ *
+ * @return array
+ * An associated array where scalar data represents a file, and arrays
+ * represent directories.
+ */
+ protected function getStore() {
+ $session = $this->getSession();
+ $store = $session->get(static::SESSION_BASE_ATTRIBUTE, []);
+ return $store;
+ }
+
+ /**
+ * Set the complete content of our session.
+ *
+ * We do this since we cannot directly probe the PHP @_SESSION variable
+ * via a PHP reference. We can only get or set automically.
+ *
+ * @param array $store
+ * The content of the whole session data store, to replace all of the
+ * current data.
+ */
+ protected function setStore(array $store) {
+ $session = $this->getSession();
+ $session->set(static::SESSION_BASE_ATTRIBUTE, $store);
+ }
+
+ /**
+ * Turn a path into the arrays we use internally.
+ *
+ * @param string $path
+ * Path into the store.
+ * @param bool $is_dir
+ * Path will be used as a container. Otherwise, path is a scalar.
+ *
+ * @return array|bool
+ * Return an array containing the "bottom" and "tip" of a directory
+ * hierarchy. You will want to save the 'bottom' array, but you may
+ * need to manipulate an object at the very tip of the hierarchy
+ * as defined in the path. The tip will be a string if we are scalar
+ * and an array otherwise. Since we don't want to create new
+ * sub arrays as a side effect, we return FALSE the intervening path
+ * does not exist.
+ */
+ public function processPath($path, $is_dir = FALSE) {
+ // We need to create a reference into the store for the point
+ // the of the path, so get a copy of the store.
+ $store = $this->getStore();
+
+ if (empty($path)) {
+ return ['store' => &$store, 'tip' => &$store];
+ }
+ $hierarchy = explode('/', $path);
+ if (empty($hierarchy) or empty($hierarchy[0])) {
+ return ['store' => &$store, 'tip' => &$store];
+ }
+ $bottom =& $store;
+ $tip = array_pop($hierarchy);
+
+ foreach ($hierarchy as $dir) {
+ if (!isset($bottom[$dir])) {
+ // If the path does not exist, DO NOT create it.
+ // That is handled by the stream wrapper code.
+ return FALSE;
+ }
+ $new_tip =& $bottom[$dir];
+ $bottom =& $new_tip;
+ }
+ // If the hierarchy was empty, just point to the object.
+ $new_tip =& $bottom[$tip];
+ $bottom =& $new_tip;
+ return ['store' => &$store, 'tip' => &$bottom];
+ }
+
+ /**
+ * The equivalent to dirname() and basename() for a path.
+ *
+ * @param string $path
+ * A file-system like path string.
+ *
+ * @return array
+ * Associative array defining an interal path of our data store. .
+ */
+ public function getParentPath($path) {
+ $dirs = explode('/', $path);
+ $tip = array_pop($dirs);
+ $parent = implode('/', $dirs);
+ return ['dirname' => $parent, 'basename' => $tip];
+ }
+
+ /**
+ * Clear a path into our store.
+ *
+ * @param string $path
+ * The path portion of a URI (i.e., without the SCHEME://).
+ */
+ public function clearPath($path) {
+ $store = $this->getStore();
+ if ($this->checkPath($path)) {
+ $path_info = $this->getParentPath($path);
+ $store_info = $this->processPath($path_info['dirname']);
+ if ($store_info === FALSE) {
+ // The path was not found, nothing to do.
+ return;
+
+ }
+ // We want to clear the key at the tip, so...
+ unset($store_info['tip'][$path_info['basename']]);
+ // Write back to the store.
+ $this->setStore($store_info['store']);
+ }
+
+ }
+
+ /**
+ * Get a path.
+ *
+ * @param string $path
+ * A URI with the SCHEME:// part removed.
+ *
+ * @return mixed
+ * Return the stored value at this "node" of the store.
+ */
+ public function getPath($path) {
+ $path_info = $this->getParentPath($path);
+ $store_info = $this->processPath(($path_info['dirname']));
+ $leaf = $path_info['basename'];
+ if ($store_info === FALSE) {
+ return NULL;
+ }
+ if ($store_info['store'] === $store_info['tip']) {
+ // We are at the top of the hierarchy; return the store itself.
+ if (empty($path_info['basename'])) {
+ return $store_info['store'];
+ }
+ if (!isset($store_info['store'][$leaf])) {
+ return NULL;
+ }
+ }
+ return $store_info['tip'][$leaf];
+ }
+
+ /**
+ * Set a path.
+ *
+ * @param string $path
+ * Path into the store.
+ * @param string|array $value
+ * Set a value.
+ */
+ public function setPath($path, $value) {
+ $path_info = $this->getParentPath($path);
+ $store_info = $this->processPath(($path_info['dirname']));
+ if ($store_info !== FALSE) {
+ $store_info['tip'][$path_info['basename']] = $value;
+ }
+ $this->setStore($store_info['store']);
+ }
+
+ /**
+ * Does path exist?
+ *
+ * @param string $path
+ * Path into the store.
+ */
+ public function checkPath($path) {
+ $path_info = $this->getParentPath($path);
+ $store_info = $this->processPath($path_info['dirname']);
+ if (empty($store_info)) {
+ // Containing directory did not exist.
+ return FALSE;
+ }
+ // Check if we are at the root of a directory.
+ if ($path_info['basename'] === '') {
+ return TRUE;
+ }
+ return isset($store_info['tip'][$path_info['basename']]);
+ }
+
+ /**
+ * Set up the store for use.
+ */
+ public function setUpStore() {
+ // Nothing to do with $_SESSION version.
+ }
+
+ /**
+ * Zero out the store.
+ */
+ public function cleanUpStore() {
+ $session = $this->getSession();
+ $session->remove(static::SESSION_BASE_ATTRIBUTE);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.info.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.info.yml
new file mode 100644
index 000000000..f9ec16409
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.info.yml
@@ -0,0 +1,13 @@
+name: Stream Wrapper Example
+type: module
+description: Example of implementing Stream Wrappers in Drupal.
+package: Example modules
+# core: 8.x
+dependencies:
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.links.menu.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.links.menu.yml
new file mode 100644
index 000000000..47fdf2bba
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.links.menu.yml
@@ -0,0 +1,4 @@
+# Menu links for the "Tools" menu.
+stream_wrapper_example.description:
+ title: Stream Wrapper Example
+ route_name: stream_wrapper_example.description
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.module b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.module
new file mode 100644
index 000000000..18309d9ac
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.module
@@ -0,0 +1,108 @@
+ [
+ 'template' => 'description',
+ 'variables' => [
+ 'admin_link' => NULL,
+ ],
+ ],
+ ];
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.routing.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.routing.yml
new file mode 100644
index 000000000..702c6c35b
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.routing.yml
@@ -0,0 +1,51 @@
+# In order to view files created with our demo stream wrapper class,
+# we need to use hook_file_download to grant any access. This route
+# will make sure that we have an external URL for these files, and that
+# our hook is called.
+#
+# In our implementation, access to the files is actually managed by
+# permissions defined in file_example.permissions.yml. Since we also want our
+# URLs to be served similar to how private: and temporary: URI are served by
+# core, we also need to modify how the routing system handles the tail portion
+# of the URL. Unlike Drupal 7, Drupal 8 does not ordinarily allow a "menu tail";
+# URLs need to be of a definite length or the router will not process them. To
+# get around this, we also implement a "path processor", which we define as a
+# service in our services file. Our path processor will do the extra steps needed
+# to process our session file URLs.
+#
+# @see stream_wrapper_example.services.yml
+# @see file_example_file_download()
+#
+stream_wrapper_example.files:
+ path: '/examples/stream_wrapper_example/files/{scheme}'
+ defaults:
+ _controller: 'Drupal\system\FileDownloadController::download'
+ scheme: session
+ requirements:
+ _access: 'TRUE'
+
+# In addition to the stream_wrapper_example.files route, which is actually matched by the router,
+# we also need a route defintion to make our URLs. This is never referenced by the
+# routing system, but is used by our stream wrapper class to create external URLs.
+#
+# @see FileExampleSessionStreamWrapper::getExternalUrl()
+#
+stream_wrapper_example.files.session:
+ path: '/examples/stream_wrapper_example/files/{filepath}'
+ defaults:
+ _controller: '\Drupal\system\FileDownloadController::download'
+ scheme: session
+ requirements:
+ # Permissive regex to allow slashes in filepath see
+ # http://symfony.com/doc/current/cookbook/routing/slash_in_parameter.html
+ filepath: .+
+ _access: 'TRUE'
+
+# Finally, our controller class.
+stream_wrapper_example.description:
+ path: '/examples/stream_wrapper_example'
+ defaults:
+ _controller: '\Drupal\stream_wrapper_example\Controller\StreamWrapperExampleController::description'
+ _title: 'Stream Wrapper Example'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.services.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.services.yml
new file mode 100644
index 000000000..b6b2db39d
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.services.yml
@@ -0,0 +1,34 @@
+#
+# As part of our demo, we implement a simple "file system" that lets us read and write
+# files out of the $_SESSION. This isn't very practical, but it's a simple way to
+# demonstrate what you can do with PHP's stream wrappers.
+#
+# To get a stream wrapper to work to define a stream wrapper class, we need to register
+# that with the system. We can either do this manually by calling up the 'stream_wrapper.manager'
+# service, but the better way to do this is to have the system autoload it by tagging the service,
+# as we do here.
+#
+# We also want to securely serve up our fake session files. We'd like to use the same nice
+# file paths that Core uses for private files. Since Drupal 8 no longer allows us to have
+# "menu tails" (i.e., extra/parts/of/the/path after the default part of the path), we need
+# to get some router superpowers. Our route (in stream_wrapper_example.routing.yml) will "gather up"
+# the path with with a regular expression. But we need to do a little more that that. We
+# also need to convince the routing system to see our weird, extra long route route. We
+# do that using a "Path Processor". We register the path_process.sessions service with special
+# tags to get it loaded for when the Drupal's routing system decides which path should get
+# used.
+#
+# @see src/StreamWrapper/FileExampleSessionStreamWrapper.php
+# @see src/PathProcessor/PathProcessorSessions.php
+# @see stream_wrapper_example.routing.yml
+#
+services:
+ stream_wrapper_example.stream_wrapper:
+ class: Drupal\stream_wrapper_example\StreamWrapper\FileExampleSessionStreamWrapper
+ tags:
+ - { name: stream_wrapper, scheme: session }
+
+ path_processor.sessions:
+ class: Drupal\stream_wrapper_example\PathProcessor\PathProcessorSessions
+ tags:
+ - { name: path_processor_inbound, priority: 200 }
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/templates/description.html.twig b/sites/all/modules/examples/examples/stream_wrapper_example/templates/description.html.twig
new file mode 100644
index 000000000..d06dd8cdb
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/templates/description.html.twig
@@ -0,0 +1,44 @@
+{#
+/**
+ * @file
+ * Contains the description text of an Example explanation/description page
+ *
+ * Available variables:
+ * - admin_link: The translated link pointing to a configuration page for the example.
+ */
+#}
+
+
+
+ {% trans %}
+
The Stream Wrapper Example module demonstrates a PHP stream wrapper implementation.
+ A stream wrapper is a class that implements something that looks and behaves like a
+ file system. A particular implementation of a stream wrapper is called a scheme.
+ Drupal 8 supports public, private, and temporary wrapper schemes. For example, you
+ access a file in your public uploads directory via a "public" file URI such as
+ public://images/big-logo.png. When you read, write, delete or move that
+ file, the public scheme's stream wrapper class
+ (\Drupal\Core\StreamWrapper\PublicStream) is invoked to do the reading,
+ writing, deletion or moving. PHP does this automatically for you, creating the wrapper
+ whenever some file operation needs to get done on a public:// file.
+
+
+
To demonstrate how to implement a stream wrapper, this example module creates a
+ session wrapper scheme. It uses your session data (created when you
+ log into Drupal) to create a nested array where the arrays represent directories,
+ and scalar values represent files. This is completely impractical, and frankly,
+ not terribly secure, so you should never enable this module on any site that's
+ open to the Internet. But without using any special libraries, our stream wrapper
+ class is able to create and delete directories, and read and write files.
+
+
+
If you want to play with session file URIs, we recommend also enabling
+ the File Example (file_example.module), which will let you do the same things with
+ the "session" scheme that you can do with public, private or temporary files.
+
+
A longer description of what code is where can be found in
+ stream_wrapper_example.module. Definitely look through the code to see
+ various implementation details.
+ {% endtrans %}
+
+
\ No newline at end of file
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php
new file mode 100644
index 000000000..6441fb3c1
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php
@@ -0,0 +1,159 @@
+root));
+ parent::setUp();
+ // Typically if we need our tested class to get information from the system,
+ // we use dependency injection (DI) to get that information to the class.
+ // But stream wrappers are unusual. They are created automatically by PHP
+ // itself when it calls one of the standard file functions, and for that
+ // reason, the constructor functions of stream wrappers cannot be passed any
+ // arguments, which prevents us from using the stardard DI technique we use
+ // in Drupal 8. The alternative is to create a "global" container that makes
+ // our services available to the class, which is what we do here.
+ $request_stack = $this->createSessionMock();
+ $this->container->set('request_stack', $request_stack);
+ $this->container->set('file_system', \Drupal::service('file_system'));
+ $this->container->set('kernel', \Drupal::service('kernel'));
+ \Drupal::setContainer($this->container);
+ }
+
+ /**
+ * Test if the session scheme was actually registered.
+ */
+ public function testSchemeRegistered() {
+ $have_session_scheme = $this->container->get('file_system')->validScheme('session');
+ $this->assertTrue($have_session_scheme, "System knows about our stream wrapper");
+ }
+
+ /**
+ * Test functions on a URI.
+ */
+ public function testReadWrite() {
+ $this->resetStore();
+ $store = $this->getCurrentStore();
+
+ $uri = 'session://drupal.txt';
+
+ $this->assertFalse(file_exists($uri), "File $uri should not exist yet.");
+ $handle = fopen($uri, 'wb');
+ $this->assertNotEmpty($handle, "Handle for $uri should be non-empty.");
+ $buffer = "Ain't seen nothin' yet!\n";
+ $len = strlen($buffer);
+
+ // Original session class gets an error here,
+ // "...stream_write wrote 10 bytes more data than requested".
+ // Does not matter for our demo, so repress error reporting here.".
+ $old = error_reporting(E_ERROR);
+ $bytes_written = @fwrite($handle, $buffer);
+ error_reporting($old);
+ $this->assertNotFalse($bytes_written, "Write to $uri succeeded.");
+
+ $rslt = fclose($handle);
+ $this->assertNotFalse($rslt, "Closed $uri.");
+ $this->assertTrue(file_exists($uri), "File $uri should now exist.");
+ $this->assertFalse(is_dir($uri), "$uri is not a directory.");
+ $this->assertTrue(is_file($uri), "$uri is a file.");
+ $size = filesize($uri);
+
+ $contents = file_get_contents($uri);
+ // The example implementation calls HTML::escape() on output. We reverse it
+ // well enough for our sample data (this code is not I18n safe).
+ $contents = Html::decodeEntities($contents);
+ $this->assertEquals($buffer, $contents, "Data for $uri should make the round trip.");
+ }
+
+ /**
+ * Directory creation.
+ */
+ public function testDirectories() {
+ $this->resetStore();
+ $dir_uri = 'session://directory1/directory2';
+ $sample_file = 'file.txt';
+ $content = "Wrote this as a file?\n";
+ $dir2 = basename($dir_uri);
+ $dir1 = dirname($dir_uri);
+
+ $this->assertFalse(file_exists($dir1), "The outer dir $dir1 should not exist yet.");
+ // We don't care about mode, since we don't support it.
+ $worked = mkdir($dir1);
+ $this->assertTrue(is_dir($dir1), "Directory $dir1 was created.");
+ $first_file_content = "This one is in the first directory.";
+ $uri = $dir1 . "/" . $sample_file;
+ $bytes = file_put_contents($uri, $first_file_content);
+ $this->assertNotFalse($bytes, "Wrote to $uri.\n");
+ $this->assertTrue(file_exists($uri), "File $uri actually exists.");
+ $got_back = file_get_contents($uri);
+ $got_back = Html::decodeEntities($got_back);
+ $this->assertSame($first_file_content, $got_back, "Data in subdir made round trip.");
+
+ // Now try down down nested.
+ $rslt = mkdir($dir_uri);
+ $this->assertTrue($rslt, "Nested dir got created.");
+ $file_in_sub = $dir_uri . "/" . $sample_file;
+ $bytes = file_put_contents($file_in_sub, $content);
+ $this->assertNotFalse($bytes, "File in nested dirs got written to.");
+ $got_back = file_get_contents($file_in_sub);
+ $got_back = Html::decodeEntities($got_back);
+ $this->assertSame($content, $got_back, "Data in subdir made round trip.");
+ $worked = unlink($file_in_sub);
+ $this->assertTrue($worked, "Deleted file in subdir.");
+ $this->assertFalse(file_exists($file_in_sub), "File in subdir should not exist.");
+ }
+
+ /**
+ * Get the contents of the complete array stored in the session.
+ */
+ protected function getCurrentStore() {
+ $handle = $this->getSessionWrapper();
+ return $handle->getPath('');
+ }
+
+ /**
+ * Clear the session storage area.
+ */
+ protected function resetStore() {
+ $handle = $this->getSessionWrapper();
+ $handle->cleanUpStore();
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php
new file mode 100644
index 000000000..be52ef721
--- /dev/null
+++ b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php
@@ -0,0 +1,101 @@
+createSessionMock();
+
+ // Set up the example.
+ $helper = new SessionWrapper($this->requestStack);
+ $helper->setUpStore();
+ }
+
+ /**
+ * Run our wrapper through the paces.
+ */
+ public function testWrapper() {
+ // Check out root.
+ $helper = new SessionWrapper($this->requestStack);
+ $root = $helper->getPath('');
+ $this->assertTrue(is_array($root), "The root is an array");
+ $this->assertTrue(empty($root), "The root is empty.");
+
+ // Add a top level file.
+ $helper = new SessionWrapper($this->requestStack);
+ $helper->setPath('drupal.txt', "Stuff");
+ $text = $helper->getPath('drupal.txt');
+ $this->assertEquals($text, "Stuff", "File at base of hierarchy can be read.");
+
+ // Add a "directory".
+ $helper = new SessionWrapper($this->requestStack);
+ $dir = [
+ 'file.txt' => 'More stuff',
+ ];
+ $helper->setPath('directory1', $dir);
+ $fetched_dir = $helper->getPath('directory1');
+ $this->assertEquals($fetched_dir['file.txt'], "More stuff", "File inside of directory can be read.");
+
+ // Check file existance.
+ $helper = new SessionWrapper($this->requestStack);
+ $this->assertTrue($helper->checkPath('drupal.txt'), "File at root still exists.");
+ $this->assertFalse($helper->checkPath('file.txt'), "Non-existant file at root does not exist.");
+ $this->assertTrue($helper->checkPath('directory1'), "Directory at root still exists.");
+ $this->assertTrue($helper->checkPath('directory1/file.txt'), "File in directory at root still exists.");
+
+ // Two deep.
+ $helper = new SessionWrapper($this->requestStack);
+ $helper->setPath('directory1/directory2', []);
+ $helper->setPath('directory1/directory2/junk.txt', "Store some junk");
+ $text = $helper->getPath('directory1/directory2/junk.txt');
+ $this->assertEquals($text, "Store some junk", "File inside of nested directory can be read.");
+
+ // Clear references.
+ $helper = new SessionWrapper($this->requestStack);
+ $before = $helper->checkPath('directory1/directory2/junk.txt');
+ $this->assertTrue($before, "File 2 deep exists.");
+ $helper->clearPath('directory1/directory2/junk.txt');
+ $after = $helper->checkPath('directory1/directory2/junk.txt');
+ $this->assertFalse($after, "File 2 deep should be gone.");
+
+ // Clean up test.
+ $helper = new SessionWrapper($this->requestStack);
+ $store = $helper->getPath('');
+ $this->assertNotEmpty($store, "Before cleanup store is not empty.");
+ $helper->cleanUpStore();
+ $store = $helper->getPath('');
+ $this->assertEmpty($store, "After cleanup store is empty.");
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Controller/TableDragExampleController.php b/sites/all/modules/examples/examples/tabledrag_example/src/Controller/TableDragExampleController.php
new file mode 100644
index 000000000..a3d7cea42
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/src/Controller/TableDragExampleController.php
@@ -0,0 +1,24 @@
+ 'table',
+ '#header' => [
+ $this->t('Name'),
+ $this->t('Description'),
+ $this->t('Weight'),
+ $this->t('Parent'),
+ ],
+ '#empty' => $this->t('Sorry, There are no items!'),
+ // TableDrag: Each array value is a list of callback arguments for
+ // drupal_add_tabledrag(). The #id of the table is automatically
+ // prepended; if there is none, an HTML ID is auto-generated.
+ '#tabledrag' => [
+ [
+ 'action' => 'match',
+ 'relationship' => 'parent',
+ 'group' => 'row-pid',
+ 'source' => 'row-id',
+ 'hidden' => TRUE, /* hides the WEIGHT & PARENT tree columns below */
+ 'limit' => FALSE,
+ ],
+ [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'row-weight',
+ ],
+ ],
+ ];
+
+ // Build the table rows and columns.
+ //
+ // The first nested level in the render array forms the table row, on which
+ // you likely want to set #attributes and #weight.
+ // Each child element on the second level represents a table column cell in
+ // the respective table row, which are render elements on their own. For
+ // single output elements, use the table cell itself for the render element.
+ // If a cell should contain multiple elements, simply use nested sub-keys to
+ // build the render element structure for drupal_render() as you would
+ // everywhere else.
+ $results = self::getData();
+ foreach ($results as $row) {
+ // TableDrag: Mark the table row as draggable.
+ $form['table-row'][$row->id]['#attributes']['class'][] = 'draggable';
+
+ // Indent item on load.
+ if (isset($row->depth) && $row->depth > 0) {
+ $indentation = [
+ '#theme' => 'indentation',
+ '#size' => $row->depth,
+ ];
+ }
+
+ // Some table columns containing raw markup.
+ $form['table-row'][$row->id]['name'] = [
+ '#markup' => $row->name,
+ '#prefix' => !empty($indentation) ? drupal_render($indentation) : '',
+ ];
+
+ $form['table-row'][$row->id]['description'] = [
+ '#type' => 'textfield',
+ '#required' => TRUE,
+ '#default_value' => $row->description,
+ ];
+
+ // This is hidden from #tabledrag array (above).
+ // TableDrag: Weight column element.
+ $form['table-row'][$row->id]['weight'] = [
+ '#type' => 'weight',
+ '#title' => $this->t('Weight for ID @id', ['@id' => $row->id]),
+ '#title_display' => 'invisible',
+ '#default_value' => $row->weight,
+ // Classify the weight element for #tabledrag.
+ '#attributes' => [
+ 'class' => ['row-weight'],
+ ],
+ ];
+ $form['table-row'][$row->id]['parent']['id'] = [
+ '#parents' => ['table-row', $row->id, 'id'],
+ '#type' => 'hidden',
+ '#value' => $row->id,
+ '#attributes' => [
+ 'class' => ['row-id'],
+ ],
+ ];
+ $form['table-row'][$row->id]['parent']['pid'] = [
+ '#parents' => ['table-row', $row->id, 'pid'],
+ '#type' => 'number',
+ '#size' => 3,
+ '#min' => 0,
+ '#title' => $this->t('Parent ID'),
+ '#default_value' => $row->pid,
+ '#attributes' => [
+ 'class' => ['row-pid'],
+ ],
+ ];
+ }
+
+ $form['actions'] = ['#type' => 'actions'];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save All Changes'),
+ ];
+ $form['actions']['cancel'] = [
+ '#type' => 'submit',
+ '#value' => 'Cancel',
+ '#attributes' => [
+ 'title' => $this->t('Return to TableDrag Overview'),
+ ],
+ '#submit' => ['::cancel'],
+ '#limit_validation_errors' => [],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Form submission handler for the 'Return to' action.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function cancel(array &$form, FormStateInterface $form_state) {
+ $form_state->setRedirect('tabledrag_example.description');
+ }
+
+ /**
+ * Submit handler for the form.
+ *
+ * Updates the 'weight' column for each element in our table, taking into
+ * account that item's new order after the drag and drop actions have been
+ * performed.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Because the form elements were keyed with the item ids from the database,
+ // we can simply iterate through the submitted values.
+ $submissions = $form_state->getValue('table-row');
+ foreach ($submissions as $id => $item) {
+ db_update('tabledrag_example')
+ ->fields([
+ 'weight' => $item['weight'],
+ 'pid' => $item['pid'],
+ 'description' => $item['description'],
+ ])
+ ->condition('id', $id, '=')
+ ->execute();
+ }
+ }
+
+ /**
+ * Retrieves the tree structure from db and sorts by parent/child/weight.
+ *
+ * The sorting should result in children items immediately following their
+ * parent items, with items at the same level of the hierarchy sorted by
+ * weight.
+ *
+ * The approach used here may be considered too database-intensive.
+ * Optimization of the approach is left as an exercise for the reader. :)
+ *
+ * @return array
+ * An associative array storing our ordered tree structure.
+ */
+ public function getData() {
+ // Get all 'root node' items (items with no parents), sorted by weight.
+ $root_items = db_select('tabledrag_example', 't')
+ ->fields('t')
+ ->condition('pid', '0', '=')
+ ->condition('id', 11, '<')
+ ->orderBy('weight')
+ ->execute()
+ ->fetchAll();
+
+ // Initialize a variable to store our ordered tree structure.
+ $tree = [];
+
+ // Depth will be incremented in our getTree()
+ // function for the first parent item, so we start it at -1.
+ $depth = -1;
+
+ // Loop through the root item, and add their trees to the array.
+ foreach ($root_items as $root_item) {
+ $this->getTree($root_item, $tree, $depth);
+ }
+
+ return $tree;
+ }
+
+ /**
+ * Recursively adds $item to $item_tree, ordered by parent/child/weight.
+ *
+ * @param mixed $item
+ * The item.
+ * @param array $tree
+ * The item tree.
+ * @param int $depth
+ * The depth of the item.
+ */
+ public function getTree($item, array &$tree = [], &$depth = 0) {
+ // Increase our $depth value by one.
+ $depth++;
+
+ // Set the current tree 'depth' for this item, used to calculate
+ // indentation.
+ $item->depth = $depth;
+
+ // Add the item to the tree.
+ $tree[$item->id] = $item;
+
+ // Retrieve each of the children belonging to this nested demo.
+ $children = db_select('tabledrag_example', 't')
+ ->fields('t')
+ ->condition('pid', $item->id, '=')
+ ->condition('id', 11, '<')
+ ->orderBy('weight')
+ ->execute()
+ ->fetchAll();
+
+ foreach ($children as $child) {
+ // Make sure this child does not already exist in the tree, to
+ // avoid loops.
+ if (!in_array($child->id, array_keys($tree))) {
+ // Add this child's tree to the $itemtree array.
+ $this->getTree($child, $tree, $depth);
+ }
+ }
+
+ // Finished processing this tree branch. Decrease our $depth value by one
+ // to represent moving to the next branch.
+ $depth--;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleResetForm.php b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleResetForm.php
new file mode 100644
index 000000000..0dfc16464
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleResetForm.php
@@ -0,0 +1,97 @@
+t('Reset demo data for TableDrag Example');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('tabledrag_example.description');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('Are you sure you want to reset demo data?');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Yes, Reset It!');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelText() {
+ return $this->t('Cancel');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Load tabledrag_example.install so that we can call
+ // tabledrag_example_data().
+ module_load_include('inc', 'tabledrag_example', 'tabledrag_example.data');
+ $data = tabledrag_example_data();
+ foreach ($data as $id => $item) {
+ // Add 1 to each array key to match ID.
+ $id++;
+ db_update('tabledrag_example')
+ ->fields([
+ 'weight' => 0,
+ 'pid' => 0,
+ 'description' => $item['description'],
+ 'itemgroup' => $item['itemgroup'],
+ ])
+ ->condition('id', $id, '=')
+ ->execute();
+ }
+ drupal_set_message($this->t('Data for TableDrag Example has been reset.'), 'status');
+ $form_state->setRedirect('tabledrag_example.description');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleRootLeafForm.php b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleRootLeafForm.php
new file mode 100644
index 000000000..edf0269e2
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleRootLeafForm.php
@@ -0,0 +1,309 @@
+ 'item',
+ '#markup' => $this->t('Tabledrag rows can be marked as roots or leaves. This limits the way the user can interact with them in drag-and-drop operations. We\'ll mark some rows this way and you can try dragging them around on the page to see how they are limited.'),
+ ];
+
+ $form['info'] = [
+ '#markup' => '
+
' . $this->t("Rows with the 'tabledrag-leaf' class cannot have child rows.") . '
+
' . $this->t("Rows with the 'tabledrag-root' class cannot be nested under a parent row.") . '
',
+ ];
+
+ $form['table-row'] = [
+ '#type' => 'table',
+ '#header' => [
+ $this->t('Name'),
+ $this->t('Description'),
+ $this->t('Weight'),
+ $this->t('Parent'),
+ ],
+ '#empty' => $this->t('Sorry, There are no items!'),
+ // TableDrag: Each array value is a list of callback arguments for
+ // drupal_add_tabledrag(). The #id of the table is automatically
+ // prepended; if there is none, an HTML ID is auto-generated.
+ '#tabledrag' => [
+ [
+ 'action' => 'match',
+ 'relationship' => 'parent',
+ 'group' => 'row-pid',
+ 'source' => 'row-id',
+ 'hidden' => TRUE, /* hides the WEIGHT & PARENT tree columns below */
+ 'limit' => FALSE,
+ ],
+ [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'row-weight',
+ ],
+ ],
+ ];
+
+ // Build the table rows and columns.
+ //
+ // The first nested level in the render array forms the table row, on which
+ // you likely want to set #attributes and #weight.
+ // Each child element on the second level represents a table column cell in
+ // the respective table row, which are render elements on their own. For
+ // single output elements, use the table cell itself for the render element.
+ // If a cell should contain multiple elements, simply use nested sub-keys to
+ // build the render element structure for drupal_render() as you would
+ // everywhere else.
+ $results = self::getData();
+ foreach ($results as $row) {
+ // TableDrag: Mark the table row as draggable.
+ $form['table-row'][$row->id]['#attributes']['class'][] = 'draggable';
+
+ // We can add the 'tabledrag-root' class to a row in order to indicate
+ // that the row may not be nested under a parent row. In our sample data
+ // for this example, the description for the item with id '11' flags it as
+ // a 'root' item which should not be nested.
+ if ($row->id == '11') {
+ $form['table-row'][$row->id]['#attributes']['class'][] = 'tabledrag-root';
+ }
+
+ // We can add the 'tabledrag-leaf' class to a row in order to indicate
+ // that the row may not contain child rows. In our sample data for this
+ // example, the description for the item with id '12' flags it as a 'leaf'
+ // item which can not contain child items.
+ if ($row->id == '12') {
+ $form['table-row'][$row->id]['#attributes']['class'][] = 'tabledrag-leaf';
+ }
+
+ // TableDrag: Sort the table row according to its existing/configured
+ // weight.
+ $form['table-row'][$row->id]['#weight'] = $row->weight;
+
+ // Indent item on load.
+ if (isset($row->depth) && $row->depth > 0) {
+ $indentation = [
+ '#theme' => 'indentation',
+ '#size' => $row->depth,
+ ];
+ }
+ // Some table columns containing raw markup.
+ $form['table-row'][$row->id]['name'] = [
+ '#markup' => $row->name,
+ '#prefix' => !empty($indentation) ? drupal_render($indentation) : '',
+ ];
+
+ $form['table-row'][$row->id]['description'] = [
+ '#type' => 'textfield',
+ '#required' => TRUE,
+ '#default_value' => $row->description,
+ ];
+
+ // This is hidden from #tabledrag array (above).
+ // TableDrag: Weight column element.
+ $form['table-row'][$row->id]['weight'] = [
+ '#type' => 'weight',
+ '#title' => $this->t('Weight for ID @id', ['@id' => $row->id]),
+ '#title_display' => 'invisible',
+ '#default_value' => $row->weight,
+ // Classify the weight element for #tabledrag.
+ '#attributes' => [
+ 'class' => ['row-weight'],
+ ],
+ ];
+ $form['table-row'][$row->id]['parent']['id'] = [
+ '#parents' => ['table-row', $row->id, 'id'],
+ '#type' => 'hidden',
+ '#value' => $row->id,
+ '#attributes' => [
+ 'class' => ['row-id'],
+ ],
+ ];
+ $form['table-row'][$row->id]['parent']['pid'] = [
+ '#parents' => ['table-row', $row->id, 'pid'],
+ '#type' => 'number',
+ '#size' => 3,
+ '#min' => 0,
+ '#title' => $this->t('Parent ID'),
+ '#default_value' => $row->pid,
+ '#attributes' => [
+ 'class' => ['row-pid'],
+ ],
+ ];
+ }
+
+ $form['actions'] = ['#type' => 'actions'];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save All Changes'),
+ ];
+ $form['actions']['cancel'] = [
+ '#type' => 'submit',
+ '#value' => 'Cancel',
+ '#attributes' => [
+ 'title' => $this->t('Return to TableDrag Overview'),
+ ],
+ '#submit' => ['::cancel'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Form submission handler for the 'Return to' action.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function cancel(array &$form, FormStateInterface $form_state) {
+ $form_state->setRedirect('tabledrag_example.description');
+ }
+
+ /**
+ * Submit handler for the form.
+ *
+ * Updates the 'weight' column for each element in our table, taking into
+ * account that item's new order after the drag and drop actions have been
+ * performed.
+ *
+ * @param array $form
+ * Render array representing from.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Current form state.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Because the form elements were keyed with the item ids from the database,
+ // we can simply iterate through the submitted values.
+ $submissions = $form_state->getValue('table-row');
+ foreach ($submissions as $id => $item) {
+ db_update('tabledrag_example')
+ ->fields([
+ 'weight' => $item['weight'],
+ 'pid' => $item['pid'],
+ 'description' => $item['description'],
+ ])
+ ->condition('id', $id, '=')
+ ->execute();
+ }
+ }
+
+ /**
+ * Retrieves the tree structure from database, sorts by parent/child/weight.
+ *
+ * The sorting should result in children items immediately following their
+ * parent items, with items at the same level of the hierarchy sorted by
+ * weight.
+ *
+ * The approach used here may be considered too database-intensive.
+ * Optimization of the approach is left as an exercise for the reader. :)
+ *
+ * @return array
+ * An associative array storing our ordered tree structure.
+ */
+ public function getData() {
+ // Get all 'root node' items (items with no parents), sorted by weight.
+ $root_items = db_select('tabledrag_example', 't')
+ ->fields('t')
+ ->condition('pid', '0', '=')
+ ->orderBy('weight')
+ ->execute()
+ ->fetchAll();
+
+ // Initialize a variable to store our ordered tree structure.
+ $tree = [];
+
+ // Depth will be incremented in our getTree()
+ // function for the first parent item, so we start it at -1.
+ $depth = -1;
+
+ // Loop through the root item, and add their trees to the array.
+ foreach ($root_items as $root_item) {
+ $this->getTree($root_item, $tree, $depth);
+ }
+
+ return $tree;
+ }
+
+ /**
+ * Recursively adds $item to $item_tree, ordered by parent/child/weight.
+ *
+ * @param mixed $item
+ * The item.
+ * @param array $tree
+ * The item tree.
+ * @param int $depth
+ * The depth of the item.
+ */
+ public function getTree($item, array &$tree = [], &$depth = 0) {
+ // Increase our $depth value by one.
+ $depth++;
+
+ // Set the current tree 'depth' for this item, used to calculate
+ // indentation.
+ $item->depth = $depth;
+
+ // Add the item to the tree.
+ $tree[$item->id] = $item;
+
+ // Retrieve each of the children belonging to this nested demo.
+ $children = db_select('tabledrag_example', 't')
+ ->fields('t')
+ ->condition('pid', $item->id, '=')
+ ->orderBy('weight')
+ ->execute()
+ ->fetchAll();
+
+ foreach ($children as $child) {
+ // Make sure this child does not already exist in the tree, to
+ // avoid loops.
+ if (!in_array($child->id, array_keys($tree))) {
+ // Add this child's tree to the $itemtree array.
+ $this->getTree($child, $tree, $depth);
+ }
+ }
+
+ // Finished processing this tree branch. Decrease our $depth value by one
+ // to represent moving to the next branch.
+ $depth--;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleSimpleForm.php b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleSimpleForm.php
new file mode 100644
index 000000000..891403d1c
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleSimpleForm.php
@@ -0,0 +1,154 @@
+ 'table',
+ '#header' => [
+ $this->t('Name'),
+ $this->t('Description'),
+ $this->t('Weight'),
+ ],
+ '#empty' => $this->t('Sorry, There are no items!'),
+ // TableDrag: Each array value is a list of callback arguments for
+ // drupal_add_tabledrag(). The #id of the table is automatically
+ // prepended; if there is none, an HTML ID is auto-generated.
+ '#tabledrag' => [
+ [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'table-sort-weight',
+ ],
+ ],
+ ];
+
+ // Build the table rows and columns.
+ //
+ // The first nested level in the render array forms the table row, on which
+ // you likely want to set #attributes and #weight.
+ // Each child element on the second level represents a table column cell in
+ // the respective table row, which are render elements on their own. For
+ // single output elements, use the table cell itself for the render element.
+ // If a cell should contain multiple elements, simply use nested sub-keys to
+ // build the render element structure for drupal_render() as you would
+ // everywhere else.
+ //
+ // About the condition id<8:
+ // For the purpose of this 'simple table' we are only using the first 8 rows
+ // of the database. The others are for 'nested' example.
+ $results = db_select('tabledrag_example', 't')
+ ->fields('t')
+ ->orderBy('weight')
+ ->condition('id', 8, '<')
+ ->execute()
+ ->fetchAll();
+ foreach ($results as $row) {
+ // TableDrag: Mark the table row as draggable.
+ $form['table-row'][$row->id]['#attributes']['class'][] = 'draggable';
+ // TableDrag: Sort the table row according to its existing/configured
+ // weight.
+ $form['table-row'][$row->id]['#weight'] = $row->weight;
+
+ // Some table columns containing raw markup.
+ $form['table-row'][$row->id]['name'] = [
+ '#markup' => $row->name,
+ ];
+ $form['table-row'][$row->id]['description'] = [
+ '#type' => 'textfield',
+ '#required' => TRUE,
+ '#default_value' => $row->description,
+ ];
+ // TableDrag: Weight column element.
+ $form['table-row'][$row->id]['weight'] = [
+ '#type' => 'weight',
+ '#title' => $this->t('Weight for @title', ['@title' => $row->name]),
+ '#title_display' => 'invisible',
+ '#default_value' => $row->weight,
+ // Classify the weight element for #tabledrag.
+ '#attributes' => ['class' => ['table-sort-weight']],
+ ];
+ }
+
+ $form['actions'] = ['#type' => 'actions'];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save All Changes'),
+ ];
+ $form['actions']['cancel'] = [
+ '#type' => 'submit',
+ '#value' => 'Cancel',
+ '#attributes' => [
+ 'title' => $this->t('Return to TableDrag Overview'),
+ ],
+ '#submit' => ['::cancel'],
+ '#limit_validation_errors' => [],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Form submission handler for the 'Return to' action.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function cancel(array &$form, FormStateInterface $form_state) {
+ $form_state->setRedirect('tabledrag_example.description');
+ }
+
+ /**
+ * Form submission handler for the simple form.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Because the form elements were keyed with the item ids from the database,
+ // we can simply iterate through the submitted values.
+ $submission = $form_state->getValue('table-row');
+ foreach ($submission as $id => $item) {
+ db_update('tabledrag_example')
+ ->fields([
+ 'weight' => $item['weight'],
+ 'description' => $item['description'],
+ ])
+ ->condition('id', $id, '=')
+ ->execute();
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.data.inc b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.data.inc
new file mode 100644
index 000000000..0da5276d4
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.data.inc
@@ -0,0 +1,77 @@
+ t('Item One'),
+ 'description' => t('The first item'),
+ 'itemgroup' => t('Group1'),
+ ],
+ [
+ 'name' => t('Item Two'),
+ 'description' => t('The second item'),
+ 'itemgroup' => t('Group1'),
+ ],
+ [
+ 'name' => t('Item Three'),
+ 'description' => t('The third item'),
+ 'itemgroup' => t('Group1'),
+ ],
+ [
+ 'name' => t('Item Four'),
+ 'description' => t('The fourth item'),
+ 'itemgroup' => t('Group2'),
+ ],
+ [
+ 'name' => t('Item Five'),
+ 'description' => t('The fifth item'),
+ 'itemgroup' => t('Group2'),
+ ],
+ [
+ 'name' => t('Item Six'),
+ 'description' => t('The sixth item'),
+ 'itemgroup' => t('Group2'),
+ ],
+ [
+ 'name' => t('Item Seven'),
+ 'description' => t('The seventh item'),
+ 'itemgroup' => t('Group3'),
+ ],
+ [
+ 'name' => t('Item Eight'),
+ 'description' => t('The eighth item'),
+ 'itemgroup' => t('Group3'),
+ ],
+ [
+ 'name' => t('Item Nine'),
+ 'description' => t('The ninth item'),
+ 'itemgroup' => t('Group3'),
+ ],
+ [
+ 'name' => t('Item Ten'),
+ 'description' => t('The tenth item'),
+ 'itemgroup' => t('Group4'),
+ ],
+ [
+ 'name' => t('Item Eleven — A Root Node'),
+ 'description' => t('This item cannot be nested under a parent item'),
+ 'itemgroup' => t('Group4'),
+ ],
+ [
+ 'name' => t('Item Twelve — A Leaf Item'),
+ 'description' => t('This item cannot have child items'),
+ 'itemgroup' => t('Group4'),
+ ],
+ ];
+ return $rows;
+}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.info.yml b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.info.yml
new file mode 100644
index 000000000..34247f3be
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.info.yml
@@ -0,0 +1,15 @@
+name: Tabledrag Example
+type: module
+description: 'Demonstrates how to create tabledrag forms.'
+package: Example modules
+configure: tabledrag_example.description
+# core: 8.x
+dependencies:
+ - examples:examples
+ - drupal:node
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.install b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.install
new file mode 100644
index 000000000..3c43957eb
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.install
@@ -0,0 +1,101 @@
+ 'Stores some entries for our tabledrag fun.',
+ 'fields' => [
+ 'id' => [
+ 'description' => 'The primary identifier for each item',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ],
+ 'name' => [
+ 'description' => 'A name for this item',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ],
+ 'description' => [
+ 'description' => 'A description for this item',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ],
+ 'itemgroup' => [
+ 'description' => 'The group this item belongs to',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ],
+ 'weight' => [
+ 'description' => 'The sortable weight for this item',
+ 'type' => 'int',
+ 'length' => 11,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ],
+ 'pid' => [
+ 'description' => 'The primary id of the parent for this item',
+ 'type' => 'int',
+ 'length' => 11,
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ],
+ ],
+ 'primary key' => ['id'],
+ ];
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ *
+ * This datafills the example items info which will be used in the example.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_install() {
+ // Insert some values into the database.
+ $rows = tabledrag_example_data();
+ if (db_table_exists('tabledrag_example')) {
+ foreach ($rows as $row) {
+ db_insert('tabledrag_example')->fields($row)->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * This removes the example data when the module is uninstalled.
+ *
+ * @ingroup tabledrag_example
+ */
+function tabledrag_example_uninstall() {
+ db_drop_table('tabledrag_example');
+}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.links.menu.yml b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.links.menu.yml
new file mode 100644
index 000000000..0c0947470
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.links.menu.yml
@@ -0,0 +1,28 @@
+tabledrag_example.description:
+ title: Tabledrag Overview
+ route_name: tabledrag_example.description
+ expanded: TRUE
+
+tabledrag_example.simple_form:
+ title: Simple rows
+ route_name: tabledrag_example.simple_form
+ parent: tabledrag_example.description
+ weight: -4
+
+tabledrag_example.parent_form:
+ title: Nested
+ route_name: tabledrag_example.parent_form
+ parent: tabledrag_example.description
+ weight: -3
+
+tabledrag_example.rootleaf_form:
+ title: Roots and leaves
+ route_name: tabledrag_example.rootleaf_form
+ parent: tabledrag_example.description
+ weight: -2
+
+tabledrag_example.reset_form:
+ title: Data reset
+ route_name: tabledrag_example.reset_form
+ parent: tabledrag_example.description
+ weight: -1
diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.module b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.module
new file mode 100644
index 000000000..5b7157316
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.module
@@ -0,0 +1,52 @@
+' . t("This module demonstrates how to code a module in order to use Drupal 8's TableDrag theme function.") . '';
+ $help_text .= '
' . t('Reference links') . '
';
+ $help_text .= '
';
+ $help_text .= '
' . $table_class_link->toString() . '
';
+ $help_text .= '
' . $table_change_link->toString() . '
';
+ $help_text .= '
' . $attach_change_link->toString() . '
';
+ $help_text .= '
';
+ $help_text .= '
' . t('To see this Example module in action:') . ' ' . $example_link->toString() . '
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/tabledrag_example/tests/src/Functional/TabledragMenuTest.php b/sites/all/modules/examples/examples/tabledrag_example/tests/src/Functional/TabledragMenuTest.php
new file mode 100644
index 000000000..8d7faeff1
--- /dev/null
+++ b/sites/all/modules/examples/examples/tabledrag_example/tests/src/Functional/TabledragMenuTest.php
@@ -0,0 +1,87 @@
+drupalLogin(
+ $this->createUser(['access content'])
+ );
+
+ $assertion = $this->assertSession();
+
+ // Get the front page, which should only have the links in the sidebar.
+ $this->drupalGet('');
+ foreach ($links as $path) {
+ $assertion->linkByHrefExists($path);
+ }
+
+ // Get each path and verify a 200 response.
+ foreach ($links as $path) {
+ $this->drupalGet($path);
+ $assertion->statusCodeEquals(200);
+ }
+
+ // Click all the submit and cancel buttons.
+ $pages = [
+ 'tabledrag_example.simple_form' => ['Save All Changes', 'Cancel'],
+ 'tabledrag_example.parent_form' => ['Save All Changes', 'Cancel'],
+ 'tabledrag_example.rootleaf_form' => ['Save All Changes', 'Cancel'],
+ 'tabledrag_example.reset_form' => ['Yes, Reset It!'],
+ ];
+ foreach ($pages as $route => $buttons) {
+ $path = Url::fromRoute($route);
+ foreach ($buttons as $button) {
+ $this->drupalPostForm($path, [], $button);
+ $assertion->statusCodeEquals(200);
+ }
+ }
+ // The reset form implements 'Cancel' as a link.
+ $this->drupalGet(Url::fromRoute('tabledrag_example.reset_form'));
+ $this->clickLink('Cancel');
+ $assertion->statusCodeEquals(200);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tablesort_example/src/Controller/TableSortExampleController.php b/sites/all/modules/examples/examples/tablesort_example/src/Controller/TableSortExampleController.php
new file mode 100644
index 000000000..7a600c296
--- /dev/null
+++ b/sites/all/modules/examples/examples/tablesort_example/src/Controller/TableSortExampleController.php
@@ -0,0 +1,87 @@
+get('database')
+ );
+ }
+
+ /**
+ * TableSortExampleController constructor.
+ *
+ * @param \Drupal\Core\Database\Connection $database
+ * The database connection.
+ */
+ public function __construct(Connection $database) {
+ $this->database = $database;
+ }
+
+ /**
+ * A simple controller method to explain what the tablesort example is about.
+ */
+ public function description() {
+ // We are going to output the results in a table with a nice header.
+ $header = [
+ // The header gives the table the information it needs in order to make
+ // the query calls for ordering. TableSort uses the field information
+ // to know what database column to sort by.
+ ['data' => t('Numbers'), 'field' => 't.numbers'],
+ ['data' => t('Letters'), 'field' => 't.alpha'],
+ ['data' => t('Mixture'), 'field' => 't.random'],
+ ];
+
+ // Using the TableSort Extender is what tells the query object that we
+ // are sorting.
+ $query = $this->database->select('tablesort_example', 't')
+ ->extend('Drupal\Core\Database\Query\TableSortExtender');
+ $query->fields('t');
+
+ // Don't forget to tell the query object how to find the header information.
+ $result = $query
+ ->orderByHeader($header)
+ ->execute();
+
+ $rows = [];
+ foreach ($result as $row) {
+ // Normally we would add some nice formatting to our rows
+ // but for our purpose we are simply going to add our row
+ // to the array.
+ $rows[] = ['data' => (array) $row];
+ }
+
+ // Build the table for the nice output.
+ $build = [
+ '#markup' => '
' . t('The layout here is a themed as a table
+ that is sortable by clicking the header name.') . '
',
+ ];
+ $build['tablesort_table'] = [
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ ];
+
+ return $build;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.info.yml b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.info.yml
new file mode 100644
index 000000000..131da5f82
--- /dev/null
+++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.info.yml
@@ -0,0 +1,14 @@
+name: Table Sort Example
+type: module
+description: 'Demonstrates how to create sortable output in a table.'
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:node
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.install b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.install
new file mode 100644
index 000000000..ac909f89a
--- /dev/null
+++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.install
@@ -0,0 +1,80 @@
+ 1, 'alpha' => 'e', 'random' => '912cv21'],
+ ['numbers' => 2, 'alpha' => 'a', 'random' => '0kuykuh'],
+ ['numbers' => 3, 'alpha' => 'm', 'random' => 'fuye8734h'],
+ ['numbers' => 4, 'alpha' => 'w', 'random' => '80jsv772'],
+ ['numbers' => 5, 'alpha' => 'o', 'random' => 'd82sf-csj'],
+ ['numbers' => 6, 'alpha' => 's', 'random' => 'au832'],
+ ['numbers' => 7, 'alpha' => 'e', 'random' => 't982hkv'],
+ ];
+
+ $db_connection = \Drupal::database();
+ if ($db_connection->schema()->tableExists('tablesort_example')) {
+ foreach ($rows as $row) {
+ db_insert('tablesort_example')->fields($row)->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ *
+ * It's good to clean up after ourselves.
+ *
+ * @ingroup tablesort_example
+ */
+function tablesort_example_uninstall() {
+ $db_connection = \Drupal::database();
+ $db_connection->schema()->dropTable('tablesort_example');
+}
+
+/**
+ * Implements hook_schema().
+ *
+ * @ingroup tablesort_example
+ */
+function tablesort_example_schema() {
+ $schema['tablesort_example'] = [
+ 'description' => 'Stores some values for sorting fun.',
+ 'fields' => [
+ 'numbers' => [
+ 'description' => 'This column simply holds numbers values',
+ 'type' => 'varchar',
+ 'length' => 2,
+ 'not null' => TRUE,
+ ],
+ 'alpha' => [
+ 'description' => 'This column simply holds alpha values',
+ 'type' => 'varchar',
+ 'length' => 2,
+ 'not null' => TRUE,
+ ],
+ 'random' => [
+ 'description' => 'This column simply holds random values',
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ ],
+ ],
+ 'primary key' => ['numbers'],
+ ];
+
+ return $schema;
+}
diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.links.menu.yml b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.links.menu.yml
new file mode 100644
index 000000000..080060793
--- /dev/null
+++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.links.menu.yml
@@ -0,0 +1,3 @@
+tablesort_example.description:
+ title: TableSort Example
+ route_name: tablesort_example_description
diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.module b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.module
new file mode 100644
index 000000000..c3b465849
--- /dev/null
+++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.module
@@ -0,0 +1,20 @@
+assertSession();
+
+ // No need to login for this test.
+ $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'desc', 'order' => 'Numbers']]);
+ $assert->statusCodeEquals(200);
+ // Ordered by number decending.
+ $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[1]');
+ $this->assertEquals(7, $item->getText(), 'Ordered by number decending.');
+
+ $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'asc', 'order' => 'Numbers']]);
+ $assert->statusCodeEquals(200);
+ // Ordered by Number ascending.
+ $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[1]');
+ $this->assertEquals(1, $item->getText(), 'Ordered by Number ascending.');
+
+ // Sort by Letters.
+ $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'desc', 'order' => 'Letters']]);
+ $assert->statusCodeEquals(200);
+ // Ordered by Letters decending.
+ $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[2]');
+ $this->assertEquals('w', $item->getText(), 'Ordered by Letters decending.');
+
+ $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'asc', 'order' => 'Letters']]);
+ $assert->statusCodeEquals(200);
+ // Ordered by Letters ascending.
+ $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[2]');
+ $this->assertEquals('a', $item->getText(), 'Ordered by Letters ascending.');
+
+ // Sort by Mixture.
+ $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'desc', 'order' => 'Mixture']]);
+ $assert->statusCodeEquals(200);
+ // Ordered by Mixture decending.
+ $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[3]');
+ $this->assertEquals('t982hkv', $item->getText(), 'Ordered by Mixture decending.');
+
+ $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'asc', 'order' => 'Mixture']]);
+ $assert->statusCodeEquals(200);
+ // Ordered by Mixture ascending.
+ $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[3]');
+ $this->assertEquals('0kuykuh', $item->getText(), 'Ordered by Mixture ascending.');
+
+ }
+
+ /**
+ * Data provider for testing menu links.
+ *
+ * @return array
+ * Array of page -> link relationships to check for.
+ * The key is the path to the page where our link should appear.
+ * The value is the link that should appear on that page.
+ */
+ protected function providerMenuLinks() {
+ return [
+ '' => '/examples/tablesort-example',
+ ];
+ }
+
+ /**
+ * Verify and validate that default menu links were loaded for this module.
+ */
+ public function testTableSortExampleLink() {
+ $assert = $this->assertSession();
+
+ $links = $this->providerMenuLinks();
+ foreach ($links as $page => $path) {
+ $this->drupalGet($page);
+ $assert->linkByHrefExists($path);
+ }
+ }
+
+ /**
+ * Tests tablesort_example menus.
+ */
+ public function testTableSortExampleMenu() {
+ $assert = $this->assertSession();
+
+ $this->drupalGet('/examples/tablesort-example');
+ $assert->statusCodeEquals(200);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/src/Controller/ContrivedController.php b/sites/all/modules/examples/examples/testing_example/src/Controller/ContrivedController.php
new file mode 100644
index 000000000..6538f88cf
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/src/Controller/ContrivedController.php
@@ -0,0 +1,93 @@
+get('string_translation')
+ );
+ }
+
+ /**
+ * Construct a new controller.
+ *
+ * @param Drupal\Core\StringTranslation\TranslationInterface $translation
+ * The translation service.
+ */
+ public function __construct(TranslationInterface $translation) {
+ $this->setStringTranslation($translation);
+ }
+
+ /**
+ * A controller method which displays a sum in terms of hands.
+ *
+ * @param int $first
+ * A parameter to the controller path.
+ * @param int $second
+ * A parameter to the controller path.
+ *
+ * @return string[]
+ * A markup array.
+ */
+ public function displayAddedNumbers($first, $second) {
+ return [
+ '#markup' => '
' . $this->handCount($first, $second) . '
',
+ ];
+ }
+
+ /**
+ * Generate a message based on how many hands are needed to count the sum.
+ *
+ * @param int $first
+ * First parameter.
+ * @param int $second
+ * Second parameter.
+ *
+ * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+ * The translated message.
+ */
+ protected function handCount($first, $second) {
+ $sum = abs($this->add((int) $first, (int) $second));
+ if ($sum <= 5) {
+ $message = $this->t('I can count these on one hand.');
+ }
+ elseif ($sum <= 10) {
+ $message = $this->t('I need two hands to count these.');
+ }
+ else {
+ $message = $this->t("That's just too many numbers to count.");
+ }
+ return $message;
+ }
+
+ /**
+ * Add two numbers.
+ *
+ * @param int $first
+ * The first parameter.
+ * @param int $second
+ * The second parameter.
+ *
+ * @return int
+ * The sum of the two parameters.
+ */
+ protected function add($first, $second) {
+ return $first + $second;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/src/Controller/TestingExampleController.php b/sites/all/modules/examples/examples/testing_example/src/Controller/TestingExampleController.php
new file mode 100644
index 000000000..b7fa02ab6
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/src/Controller/TestingExampleController.php
@@ -0,0 +1,26 @@
+assertTrue(TRUE);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/templates/description.html.twig b/sites/all/modules/examples/examples/testing_example/templates/description.html.twig
new file mode 100644
index 000000000..378291464
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/templates/description.html.twig
@@ -0,0 +1,79 @@
+{#
+
+Description text for the PHPUnit Example.
+
+#}
+
+{% trans %}
+
Testing Frameworks in Drupal
+
+
How to use this example module
+
You really should be reading the various docblocks in the files under
+ tests/src/.
+
+
How To:
+
+
PHPUnit-based Drupal tests go in the tests/src directory, so
+ they will not be loaded by the autoloader during normal bootstrap.
+
+
+
Unit tests go in [your_module]/tests/src/Unit.
+
Kernel tests go in [your_module]/tests/src/Kernel.
+
Functional tests go in [your_module]/tests/src/Functional.
+
+
+
+
+
+
Type
+
Location
+
Namespace
+
Subclass of
+
+
+
+
+
Unit test
+
[your_module]/tests/src/Unit
+
Drupal\Tests\[your_module]\Unit\
+
Drupal\Tests\UnitTestCase
+
+
+
Kernel test
+
[your_module]/tests/src/Kernel
+
Drupal\Tests\[your_module]\Kernel\
+
Drupal\KernelTests\KernelTestBase
+
+
+
Functional test
+
[your_module]/tests/src/Functional
+
Drupal\Tests\[your_module]\Functional\
+
Drupal\Tests\BrowserTestBase
+
+
+
FunctionalJavascript test
+
[your_module]/tests/src/FunctionalJavascript
+
Drupal\Tests\[your_module]\FunctionalJavascript\
+
Drupal\Tests\JavascriptTestBase
+
+
+
Shared test traits
+
[your_module]/tests/src/Traits
+
Drupal\Tests\[your_module]\Traits\
+
n/a
+
+
+
+
+
+
Standard PHPUnit Practices
+
You can run PHPUnit from the command line or via the run-tests.sh command.
+
You can specify which type of test you're running via
+
phpunit --testsuite
and
+
run-tests.sh --types
.
+
There are many options to phpunit, but, for instance, to run all of the
+ testing_example phpunit tests, you could
+
+
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/testing_example/testing_example.info.yml b/sites/all/modules/examples/examples/testing_example/testing_example.info.yml
new file mode 100644
index 000000000..457ba16fb
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/testing_example.info.yml
@@ -0,0 +1,13 @@
+name: Testing Example
+type: module
+description: Demonstrates Drupal core testing frameworks
+package: Example modules
+# core: 8.x
+dependencies:
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/testing_example/testing_example.links.menu.yml b/sites/all/modules/examples/examples/testing_example/testing_example.links.menu.yml
new file mode 100644
index 000000000..f5b775a3e
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/testing_example.links.menu.yml
@@ -0,0 +1,8 @@
+testing_example.description:
+ title: Testing Example
+ route_name: testing_example.description
+ expanded: TRUE
+testing_example.sum_in_hands:
+ title: Sum in hands
+ route_name: testing_example.sum_in_hands
+ parent: testing_example.description
diff --git a/sites/all/modules/examples/examples/testing_example/testing_example.module b/sites/all/modules/examples/examples/testing_example/testing_example.module
new file mode 100644
index 000000000..e44d99b8f
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/testing_example.module
@@ -0,0 +1,17 @@
+adminUser = $this->drupalCreateUser([
+ 'access administration pages',
+ 'view the administration theme',
+ 'administer permissions',
+ 'administer nodes',
+ 'administer content types',
+ ]);
+ $this->authUser = $this->drupalCreateUser([], 'authuser');
+
+ // We have to create a content type because testing uses the 'testing'
+ // profile, which has no content types by default.
+ // Although we could have visited admin pages and pushed buttons to create
+ // the content type, there happens to be function we can use in this case.
+ $this->createContentType(['type' => 'test_content_type']);
+ }
+
+ /**
+ * Demonstrate node creation through UI interaction.
+ */
+ public function testNewPage() {
+ // We log in an administrator because they will have permissions to create
+ // content.
+ $this->drupalLogin($this->adminUser);
+
+ // For many assertions, we need a WebAssert object. This object gives us
+ // assertion types for the HTTP requests we make, such as content and the
+ // HTTP status code.
+ /** @var \Drupal\Tests\WebAssert $assert */
+ $assert = $this->assertSession();
+
+ // Get the page that lets us add new content.
+ $this->drupalGet('node/add/test_content_type');
+ // Use the WebAssert object to assert the HTTP status code.
+ $assert->statusCodeEquals(200);
+
+ // Set up our new piece of content.
+ $nodeTitle = 'Test node for testNewPage';
+ $edit = [
+ 'title[0][value]' => $nodeTitle,
+ 'body[0][value]' => 'Body of test node',
+ ];
+ // Tell Drupal to post our new content. We post to NULL for the URL which
+ // tells drupalPostForm() to use the current page.
+ $this->drupalPostForm(NULL, $edit, 'op');
+ // Check our expectations.
+ $assert->statusCodeEquals(200);
+ $assert->linkExists($nodeTitle);
+
+ // Log in our non-admin user and navigate to the node.
+ $this->drupalLogin($this->authUser);
+
+ // We can search for the node by its title. Since the node object can also
+ // tell us its URL, we can just feed that information into drupalGet().
+ /** @var \Drupal\node\NodeInterface $createdNode */
+ $createdNode = $this->drupalGetNodeByTitle($nodeTitle);
+ $url = $createdNode->toUrl();
+ $this->drupalGet($url);
+ $assert->statusCodeEquals(200);
+
+ // Look at the page title.
+ $assert->titleEquals("{$nodeTitle} | Drupal");
+
+ // Find the title of the node itself.
+ $nodeTitleElement = $this->getSession()
+ ->getPage()
+ ->find('css', 'h1 span.field--name-title');
+ $this->assertEquals($nodeTitleElement->getText(), $nodeTitle);
+ }
+
+ /**
+ * Demonstrate node creation via NodeCreationTrait::createNode.
+ */
+ public function testNewPageApiCreate() {
+ $assert = $this->assertSession();
+
+ $this->drupalLogin($this->adminUser);
+
+ $nodeTitle = 'Test node for testNewPageApiCreate';
+
+ // Create new node using API.
+ $node = $this->drupalCreateNode([
+ 'type' => 'test_content_type',
+ 'title' => $nodeTitle,
+ 'body' => [
+ [
+ 'format' => filter_default_format($this->adminUser),
+ 'value' => 'Body of test node',
+ ],
+ ],
+ ]);
+ $node->save();
+ $url = $node->toUrl();
+
+ // Confirm page creation.
+ $this->drupalGet($url);
+ $assert->statusCodeEquals(200);
+
+ // Log in our normal user and navigate to the node.
+ $this->drupalLogin($this->authUser);
+ $this->drupalGet($url);
+ $assert->statusCodeEquals(200);
+
+ // Look at the *page* title.
+ $assert->titleEquals("{$nodeTitle} | Drupal");
+
+ // Find the title of the node itself.
+ $nodeTitleElement = $this->getSession()
+ ->getPage()
+ ->find('css', 'h1 span.field--name-title');
+ $this->assertEquals($nodeTitleElement->getText(), $nodeTitle);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkDependenciesTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkDependenciesTest.php
new file mode 100644
index 000000000..178804860
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkDependenciesTest.php
@@ -0,0 +1,69 @@
+placeBlock('system_menu_block:tools');
+ // Add a content type.
+ $this->contentType = $this->createContentType();
+ }
+
+ /**
+ * Tests for the existence of a default menu item on the home page.
+ *
+ * We'll open the home page and look for the Tools menu link called 'Add
+ * content.'
+ */
+ public function testAddContentMenuItem() {
+ // Step 1: Log in a user who can add content.
+ $this->drupalLogin(
+ $this->createUser([
+ 'create ' . $this->contentType->id() . ' content',
+ ])
+ );
+
+ // Step 2: Visit the home path.
+ $this->drupalGet($this->buildUrl(''));
+ // Step 3: Look on the page for the 'Add content' link.
+ $this->assertSession()->linkExists('Add content');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkTest.php
new file mode 100644
index 000000000..9dedaf6c6
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkTest.php
@@ -0,0 +1,43 @@
+drupalLogin(
+ $this->createUser([
+ 'create article content',
+ ])
+ );
+
+ // Step 2: Visit the home path.
+ $this->drupalGet($this->buildUrl(''));
+ // Step 3: Look on the page for the 'Add content' link.
+ $this->assertSession()->linkExists('Add content');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/SkeletonTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/SkeletonTest.php
new file mode 100644
index 000000000..1f997a01e
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/SkeletonTest.php
@@ -0,0 +1,80 @@
+fail('The test runner found our test and failed it. Yay!');
+ //
+ // PHPUnit will complain if there is no assertion in our test method, so we
+ // add this passing assertion. Comment or delete it if you'd like.
+ $this->assertTrue(TRUE);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/TestingExampleMenuTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/TestingExampleMenuTest.php
new file mode 100644
index 000000000..35bfb8fbb
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/TestingExampleMenuTest.php
@@ -0,0 +1,49 @@
+ '/examples/testing-example'] as $page => $path) {
+ $this->drupalGet($page);
+ $this->assertLinkByHref($path);
+ }
+ $this->drupalGet('/examples/testing-example');
+ $this->assertResponse(200, 'Description page exists.');
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Kernel/ExampleFixtureManagementTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Kernel/ExampleFixtureManagementTest.php
new file mode 100644
index 000000000..d3cdddbfa
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/tests/src/Kernel/ExampleFixtureManagementTest.php
@@ -0,0 +1,121 @@
+installSchema('system', ['sequences']);
+
+ // Install *module* schema for node/user modules.
+ $this->installSchema('node', ['node_access']);
+ $this->installSchema('user', ['users_data']);
+
+ // Install *entity* schema for the node entity.
+ $this->installEntitySchema('node');
+ $this->installEntitySchema('user');
+
+ // Install any config provided by the enabled.
+ $this->installConfig(['field', 'node', 'text', 'filter', 'user']);
+
+ // Finally, create an 'owner' account.
+ $this->owner = $this->createUser([], 'testuser');
+ }
+
+ /**
+ * Create a node by using createNode() from NodeCreationTrait.
+ */
+ public function testNodeCreation() {
+ // Unless there's a specific reason to do so, strings in tests should not be
+ // translated with t().
+ $nodeTitle = 'Test Node!';
+
+ /** @var \Drupal\node\NodeInterface $node */
+ $node = $this->createNode([
+ 'title' => $nodeTitle,
+ 'type' => 'page',
+ 'uid' => $this->owner->id(),
+ ]);
+
+ // Assert that the node we created has the title we expect.
+ $this->assertEquals($nodeTitle, $node->getTitle());
+ }
+
+ /**
+ * Create a user account using createUser() from the UserCreation trait.
+ */
+ public function testUserCreation() {
+ // Create a user named 'extrauser'.
+ $account = $this->createUser([], 'extrauser');
+ // Assert that this user exists.
+ $this->assertEquals('extrauser', $account->getAccountName());
+
+ // Assert that our auth user is not the same user as extrauser.
+ $this->assertNotEquals($this->owner->getAccountName(), $account->getAccountName());
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Unit/Controller/ContrivedControllerTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Unit/Controller/ContrivedControllerTest.php
new file mode 100644
index 000000000..0d060b402
--- /dev/null
+++ b/sites/all/modules/examples/examples/testing_example/tests/src/Unit/Controller/ContrivedControllerTest.php
@@ -0,0 +1,118 @@
+getMockBuilder(ContrivedController::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $ref_add = new \ReflectionMethod($controller, 'add');
+ $ref_add->setAccessible(TRUE);
+ $this->assertEquals($expected, $ref_add->invokeArgs($controller, [$first, $second]));
+ }
+
+ /**
+ * Data provider for testHandCount().
+ */
+ public function provideTestHandCount() {
+ return [
+ ['I can count these on one hand.', 0, 0],
+ ['I can count these on one hand.', 1, 0],
+ ['I can count these on one hand.', 0, 1],
+ ['I need two hands to count these.', 5, 5],
+ ['That\'s just too many numbers to count.', 5, 6],
+ ['That\'s just too many numbers to count.', 6, 5],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTestHandCount
+ */
+ public function testHandCount($expected, $first, $second) {
+ // Get a mock translation service.
+ $mock_translation = $this->getStringTranslationStub();
+ // Create a new controller with our mocked translation service.
+ $controller = new ContrivedController($mock_translation);
+
+ // Set up a reflection for handCount().
+ $ref_hand_count = new \ReflectionMethod($controller, 'handCount');
+ // Set handCount() to be public.
+ $ref_hand_count->setAccessible(TRUE);
+ // Check out whether handCount() meets our expectation.
+ $message = $ref_hand_count->invokeArgs($controller, [$first, $second]);
+ $this->assertEquals($expected, (string) $message);
+ }
+
+ /**
+ * Data provider for testHandCountIsolated().
+ */
+ public function providerTestHandCountIsolated() {
+ $data = [];
+
+ // Add one-hand data.
+ foreach (range(0, 5) as $sum) {
+ $data[] = ['I can count these on one hand.', $sum];
+ }
+
+ // Add two-hand data.
+ foreach (range(6, 10) as $sum) {
+ $data[] = ['I need two hands to count these.', $sum];
+ }
+
+ // Add too-many data.
+ foreach (range(11, 15) as $sum) {
+ $data[] = ['That\'s just too many numbers to count.', $sum];
+ }
+
+ return $data;
+ }
+
+ /**
+ * @dataProvider providerTestHandCountIsolated
+ */
+ public function testHandCountIsolated($expected, $sum) {
+ // Mock a ContrivedController, using a mocked translation service.
+ $controller = $this->getMockBuilder(ContrivedController::class)
+ ->setConstructorArgs([$this->getStringTranslationStub()])
+ // Specify that we'll also mock add().
+ ->setMethods(['add'])
+ ->getMock();
+
+ // Mock add() so that it returns our $sum when it's called with (0,0).
+ $controller->expects($this->once())
+ ->method('add')
+ ->with($this->equalTo(0), $this->equalTo(0))
+ ->willReturn($sum);
+
+ // Use reflection to make handCount() public.
+ $ref_hand_count = new \ReflectionMethod($controller, 'handCount');
+ $ref_hand_count->setAccessible(TRUE);
+
+ // Invoke handCount().
+ $message = (string) $ref_hand_count->invokeArgs($controller, [0, 0]);
+
+ // Assert our expectations.
+ $this->assertEquals($expected, $message);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.info.yml b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.info.yml
new file mode 100644
index 000000000..0067bbb45
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.info.yml
@@ -0,0 +1,13 @@
+name: 'Sample Description Template Test'
+type: module
+description: 'Support module for testing the DescriptionTemplateTrait.'
+package: Testing
+# core: 8.x
+dependencies:
+ - examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.routing.yml b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.routing.yml
new file mode 100644
index 000000000..685d52820
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.routing.yml
@@ -0,0 +1,6 @@
+example_description_trait_test.description:
+ path: 'examples/tests/example-description'
+ defaults:
+ _controller: '\Drupal\examples_description_test\Controller\SampleExampleController::description'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/src/Controller/SampleExampleController.php b/sites/all/modules/examples/examples/tests/modules/examples_description_test/src/Controller/SampleExampleController.php
new file mode 100644
index 000000000..218e45f8b
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/src/Controller/SampleExampleController.php
@@ -0,0 +1,34 @@
+ $this->getModuleName(),
+ 'slogan' => $this->t('We aim to please'),
+ ];
+ return $variables;
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/templates/description.html.twig b/sites/all/modules/examples/examples/tests/modules/examples_description_test/templates/description.html.twig
new file mode 100644
index 000000000..3e8d3971a
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/templates/description.html.twig
@@ -0,0 +1,25 @@
+{*
+
+Description template test example.
+
+This is a test template that demonstrates how an Examples module can
+implement its description controller by using a Twig template.
+
+*}
+
+
+
+{% trans %}
+
+
Sample Description
+
+
Here is a sample description. It embeds some Twig variables.
+
+
+
Used in module: {{ module }}.
+
Our slogan for today: {{slogan}}.
+
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/tests/src/Functional/ExamplesBrowserTestBase.php b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesBrowserTestBase.php
new file mode 100644
index 000000000..70c599d51
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesBrowserTestBase.php
@@ -0,0 +1,47 @@
+setupExamplesMenus();
+ }
+
+ /**
+ * Set up menus and tasks in their regions.
+ *
+ * Since menus and tasks are now blocks, we're required to explicitly set them
+ * to regions. This method standardizes the way we do that for Examples.
+ *
+ * Note that subclasses must explicitly declare that the block module is a
+ * dependency.
+ */
+ protected function setupExamplesMenus() {
+ $this->drupalPlaceBlock('system_menu_block:tools', ['region' => 'primary_menu']);
+ $this->drupalPlaceBlock('local_tasks_block', ['region' => 'secondary_menu']);
+ $this->drupalPlaceBlock('local_actions_block', ['region' => 'content']);
+ $this->drupalPlaceBlock('page_title_block', ['region' => 'content']);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tests/src/Functional/ExamplesTest.php b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesTest.php
new file mode 100644
index 000000000..c503a715d
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesTest.php
@@ -0,0 +1,74 @@
+assertSession();
+
+ // Verify that the toolbar tab and tray are showing and functioning.
+ $user = $this->drupalCreateUser(['access toolbar']);
+ $this->drupalLogin($user);
+
+ // Check for the 'Examples' tab.
+ $this->drupalGet('');
+ $assert->statusCodeEquals(200);
+
+ // Assert that the toolbar tab registered by examples is present.
+ $assert->linkExists('Examples');
+
+ // Assert that the toolbar tab registered by examples is present.
+ $this->assertEquals(
+ 1,
+ \count($this->xpath('//nav/div/a[@data-toolbar-tray="toolbar-item-examples-tray"]')),
+ 'Found the Examples toolbar tab.'
+ );
+
+ // Assert that the toolbar tray registered by examples is present.
+ $this->assertEquals(
+ 1,
+ \count($this->xpath('//nav/div/div[@data-toolbar-tray="toolbar-item-examples-tray"]')),
+ 'Found the Examples toolbar tray.'
+ );
+ // Assert that PHPUnit link does not appears in the tray.
+ $phpunit_link = 'PHPUnit Example';
+ $assert->linkNotExists($phpunit_link);
+ $assert->pageTextNotContains('
');
+
+ // Install phpunit_example and see if it appears in the toolbar. We use
+ // phpunit_example because it's very light-weight.
+ $this->container->get('module_installer')->install(['phpunit_example'], TRUE);
+ // SimpleTest needs for us to reset all the caches.
+ $this->resetAll();
+
+ // Verify that PHPUnit appears in the tray.
+ $this->drupalGet('');
+ $assert->linkExists($phpunit_link);
+ // Assert that the PHPUnit tray item is present.
+ $this->assertEquals(
+ 1,
+ \count($this->xpath('//nav/div/div/nav/ul/li[@class="phpunit-example"]')),
+ 'Found the PHPUnit Example tray item.'
+ );
+
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tests/src/Kernel/DescriptionTraitTest.php b/sites/all/modules/examples/examples/tests/src/Kernel/DescriptionTraitTest.php
new file mode 100644
index 000000000..817fcb7de
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/src/Kernel/DescriptionTraitTest.php
@@ -0,0 +1,41 @@
+container);
+ // We want to test ::getDescriptionTemplatePath(), which is a protected
+ // method. Use a little of the Old Black Reflection Magic.
+ $ref_get_path = new \ReflectionMethod($sample_controller, 'getDescriptionTemplatePath');
+ $ref_get_path->setAccessible(TRUE);
+ $this->assertFileExists($ref_get_path->invoke($sample_controller));
+ // And get our render output.
+ $render_array = $sample_controller->description();
+ // We cast to string, since renderPlain() returns a markup object.
+ $output = (string) $this->container->get('renderer')->renderPlain($render_array);
+ // Did the template load?
+ $this->assertContains('Template loaded!', $output);
+ // Were the variables resolved correctly?
+ $this->assertContains('Used in module: examples_description_test.', $output);
+ $this->assertContains('Our slogan for today: We aim to please.', $output);
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tests/src/Unit/YamlValidationTest.php b/sites/all/modules/examples/examples/tests/src/Unit/YamlValidationTest.php
new file mode 100644
index 000000000..1f6b5c532
--- /dev/null
+++ b/sites/all/modules/examples/examples/tests/src/Unit/YamlValidationTest.php
@@ -0,0 +1,49 @@
+getPathname();
+ if (strpos($pathname, '.yml') !== FALSE) {
+ if (strpos($pathname, '/config/') !== FALSE) {
+ $yaml_paths[] = [$pathname];
+ }
+ }
+ }
+ return $yaml_paths;
+ }
+
+ /**
+ * @dataProvider provideYamls
+ */
+ public function testNoUuidsInConfig($yaml_path) {
+ $yaml = Yaml::parse(file_get_contents($yaml_path));
+ $this->assertArrayNotHasKey('uuid', $yaml, "YAML in this file contains a uuid key: $yaml_path");
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tour_example/config/install/tour.tour.tour-example.yml b/sites/all/modules/examples/examples/tour_example/config/install/tour.tour.tour-example.yml
new file mode 100644
index 000000000..b795e1b96
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/config/install/tour.tour.tour-example.yml
@@ -0,0 +1,103 @@
+# This file defines the tour for our example page. There should be one tour
+# file for each tour that you create. These tour files should be placed in a
+# module's 'config' folder and named using the pattern
+# 'tour.tour.{tour-id}.yml'.
+#
+# Each tour file has two parts: 1) The tour properties, and 2) the tip
+# definitions.
+#
+#
+# TOUR PROPERTIES
+#
+# The tour properties define information that applies to the tour as a whole.
+# Properties that you can define for your tours include the following:
+#
+# id: Each tour should have a unique ID. This id is used in the
+# filename and appears as an ID in the HTML.
+# module: The machine name of the module containing your tour.
+# label: A human readable name for the tour.
+# status: A boolean, defining whether the tour is enabled.
+# langcode: A two-letter language code defining the language of your tour.
+# routes: An array of routes for which the tour is active. Specify these
+# as an array with route_name and optional route_params (also an
+# array). Route names are found in each module's routing.yml file.
+#
+#
+# TIP DEFINITIONS
+#
+# Here, you define each tip that you want to appear in your tour. Each of
+# the tips have properties that must be defined, including the following:
+#
+# id: Each tip needs a unique ID. This appears as an ID in the HTML.
+# plugin: The Tour API uses plugins for defining different types of
+# tips. The 'text' plugin (for making text tooltips) is provided
+# in core, but developers can define additional plugins for tips
+# containing images, video, or other interactions.
+# label: The lable for the tip. This will be rendered in an h3 element.
+# body: The body of the tip. HTML markup is allowed.
+# weight: Tips within a tour are ordered by weight, beginning with the
+# lowest number. Negative values are acceptable.
+# location: Defines the location of the tip, relative to its target.
+# Acceptable values include: top, bottom, left, & right. If the
+# location is not defined, the default value (bottom) will be
+# used.
+# attributes: Attributes provdied to the tip for various purposes. Use the
+# following attributes to control the placement of the tip:
+#
+# data-id: Places this tip on the DOM element containing
+# this ID.
+# data-class: Places this tip on the DOM element containing
+# this class.
+#
+# If you omit both the .data-id and .data-class, the tip will be
+# shown as modal instead of being targeted to an element.
+#
+id: tour-example
+module: tour_example
+label: 'Tour an example admin page'
+langcode: en
+routes:
+ - route_name: tour_example_description
+tips:
+ introduction:
+ id: introduction
+ plugin: text
+ label: 'Introduction'
+ body: 'This is an example tour. Click "next" to continue through the tour.'
+ weight: 1
+ first-item:
+ id: first-item
+ plugin: text
+ label: 'First Item'
+ body: 'Tours are helpful for walking users through an unfamiliar admin interface.'
+ weight: 2
+ location: bottom
+ attributes:
+ data-id: tour-id-1
+ second-item:
+ id: second-item
+ plugin: text
+ label: 'Second Item'
+ body: 'Individual tips can be positioned anywhere on the page.'
+ weight: 3
+ location: top
+ attributes:
+ data-id: tour-id-2
+ third-item:
+ id: third-item
+ plugin: text
+ label: 'Third Item'
+ body: 'You can click the X in the top right corner of this box to close the tour at any time.'
+ weight: 4
+ location: bottom
+ attributes:
+ data-id: tour-id-3
+ fourth-item:
+ id: fourth-item
+ plugin: text
+ label: 'Fourth Item'
+ body: 'When the tour is over, click "End Tour".'
+ weight: 5
+ location: left
+ attributes:
+ data-id: tour-id-4
diff --git a/sites/all/modules/examples/examples/tour_example/src/Controller/TourExampleController.php b/sites/all/modules/examples/examples/tour_example/src/Controller/TourExampleController.php
new file mode 100644
index 000000000..7eb50ed60
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/src/Controller/TourExampleController.php
@@ -0,0 +1,24 @@
+The Tour:
+
Click the 'Tour' icon in the admin menu bar to start.
+
First item.
+
Second item.
+
Third item.
+
Fourth item.
+
+
About Tours
+
+
The Tour module allows you you make instructional tours of user interfaces.
+
+
The Tour module comes with Drupal 8 and makes it easy for developers to add
+ "Tours" for guiding users through unfamiliar user interfaces.
+
+
Each tour is comprised of a series of tooltips that provide contextual
+ information about an interface. The user can start a tour by clicking the
+ "Tour" icon on the right side of the Drupal 8 toolbar. The tour icon is only
+ visible when there is a tour available on the current page.
+
+
The Tour module provides the Tour API, which makes it easy for developers to
+ add tours to their modules. In most cases, adding a tour is as simple as
+ creating a YAML file in the config directory in their module, containing the
+ expected data. For a detailed example of such a file, see
+ config/install/tour.tour.tour-example.yml.
+
+
If you are interested in building tours through a user interface, you may
+ want to look at the Tour UI module: https://drupal.org/project/tour_ui
+
+
The Tour module uses the Joyride jQuery plugin for its underlying
+ functionality. You can find more information about Joyride at
+ https://github.com/zurb/joyride
+
+{% endtrans %}
diff --git a/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourExampleTest.php b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourExampleTest.php
new file mode 100644
index 000000000..672406be4
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourExampleTest.php
@@ -0,0 +1,54 @@
+assertSession();
+
+ // Test for a link to the tour_example in the Tools menu.
+ $this->drupalGet('');
+ $assert->statusCodeEquals(200);
+ $assert->linkByHrefExists('examples/tour-example');
+
+ // Verify if the can successfully access the tour_examples page.
+ $this->drupalGet('examples/tour-example');
+ $assert->statusCodeEquals(200);
+
+ // Verify that the tour tips exist on this page.
+ $this->assertTourTips();
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBase.php b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBase.php
new file mode 100644
index 000000000..d9c703f0e
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBase.php
@@ -0,0 +1,82 @@
+assertTourTips();
+ *
+ * // Advanced example. The following would be used for multipage or
+ * // targeting a specific subset of tips.
+ * $tips = array();
+ * $tips[] = array('data-id' => 'foo');
+ * $tips[] = array('data-id' => 'bar');
+ * $tips[] = array('data-class' => 'baz');
+ * $this->assertTourTips($tips);
+ * @endcode
+ */
+ public function assertTourTips(array $tips = []) {
+ $assert = $this->assertSession();
+ // Get the rendered tips and their data-id and data-class attributes.
+ if (empty($tips)) {
+ // Tips are rendered as
elements inside .
+ $rendered_tips = $this->xpath('//ol[@id = "tour"]//li[starts-with(@class, "tip")]');
+ foreach ($rendered_tips as $rendered_tip) {
+ $item = [];
+ if ($rendered_tip->hasAttribute('data-id')) {
+ $item['data-id'] = $rendered_tip->getAttribute('data-id');
+ }
+ if ($rendered_tip->hasAttribute('data-class')) {
+ $item['data-class'] = $rendered_tip->getAttribute('data-class');
+ }
+ if (!empty($item)) {
+ $tips[] = $item;
+ }
+ }
+ }
+
+ // If the tips are still empty we need to fail.
+ if (empty($tips)) {
+ $this->fail('Could not find tour tips on the current page.');
+ }
+ else {
+ // Check for corresponding page elements.
+ $total = 0;
+ $modals = 0;
+ foreach ($tips as $tip) {
+ if (!empty($tip['data-id'])) {
+ $elements = $this->getSession()->getPage()->find('css', "#{$tip['data-id']}");
+ $this->assertTrue(!empty($elements) && count($elements) === 1, format_string('Found corresponding page element for tour tip with id #%data-id', ['%data-id' => $tip['data-id']]));
+ }
+ elseif (!empty($tip['data-class'])) {
+ $elements = $this->getSession()->getPage()->find('css', "#{$tip['data-class']}");
+
+ $this->assertFalse(empty($elements), format_string('Found corresponding page element for tour tip with class .%data-class', ['%data-class' => $tip['data-class']]));
+ }
+ else {
+ // It's a modal.
+ $modals++;
+ }
+ $total++;
+ }
+ $this->verbose(format_string('Total %total Tips tested of which %modals modal(s).', ['%total' => $total, '%modals' => $modals]));
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBasic.php b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBasic.php
new file mode 100644
index 000000000..a2063780d
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBasic.php
@@ -0,0 +1,76 @@
+ array(
+ * array('data-id' => 'foo'),
+ * array('data-class' => 'bar'),
+ * ),
+ * );
+ * @endcode
+ */
+ protected $tips = [];
+
+ /**
+ * An admin user with administrative permissions for tour.
+ *
+ * @var \Drupal\user\UserInterface
+ */
+ protected $adminUser;
+
+ /**
+ * The permissions required for a logged in user to test tour tips.
+ *
+ * @var array
+ * A list of permissions.
+ */
+ protected $permissions = ['access tour'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // Make sure we are using distinct default and administrative themes for
+ // the duration of these tests.
+ $this->container->get('theme_handler')->install(['bartik', 'seven']);
+ $this->config('system.theme')
+ ->set('default', 'bartik')
+ ->set('admin', 'seven')
+ ->save();
+
+ $this->permissions[] = 'view the administration theme';
+
+ // Create an admin user to view tour tips.
+ $this->adminUser = $this->drupalCreateUser($this->permissions);
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * A simple tip test.
+ */
+ public function testTips() {
+ foreach ($this->tips as $path => $attributes) {
+ $this->drupalGet($path);
+ $this->assertTourTips($attributes);
+ }
+ }
+
+}
diff --git a/sites/all/modules/examples/examples/tour_example/tour_example.info.yml b/sites/all/modules/examples/examples/tour_example/tour_example.info.yml
new file mode 100644
index 000000000..fa44f204a
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/tour_example.info.yml
@@ -0,0 +1,15 @@
+name: 'Tour Example'
+type: module
+description: 'Demonstrates how to create a tour in Drupal 8.'
+package: Example modules
+# core: 8.x
+dependencies:
+ - drupal:tour
+ - drupal:toolbar
+ - examples:examples
+
+# Information added by Drupal.org packaging script on 2017-12-17
+version: '8.x-1.x-dev'
+core: '8.x'
+project: 'examples'
+datestamp: 1513537386
diff --git a/sites/all/modules/examples/examples/tour_example/tour_example.links.menu.yml b/sites/all/modules/examples/examples/tour_example/tour_example.links.menu.yml
new file mode 100644
index 000000000..70d574dc1
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/tour_example.links.menu.yml
@@ -0,0 +1,3 @@
+tour_example.description:
+ title: Tour Example
+ route_name: tour_example_description
diff --git a/sites/all/modules/examples/examples/tour_example/tour_example.module b/sites/all/modules/examples/examples/tour_example/tour_example.module
new file mode 100644
index 000000000..b033d08e8
--- /dev/null
+++ b/sites/all/modules/examples/examples/tour_example/tour_example.module
@@ -0,0 +1,38 @@
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/examples/ms_ajax_form_example/README.txt b/sites/all/modules/examples/ms_ajax_form_example/README.txt
new file mode 100644
index 000000000..8bdf49ce9
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/README.txt
@@ -0,0 +1 @@
+Multi Step Form Example
diff --git a/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.info.yml b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.info.yml
new file mode 100644
index 000000000..7343c7eb2
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.info.yml
@@ -0,0 +1,12 @@
+name: Multi Step Form Example
+type: module
+description: 'Defines framework to create ajax based multi step forms.'
+# core: 8.x
+package: Custom
+configure: ms_ajax_form_example.page
+
+# Information added by Drupal.org packaging script on 2017-05-28
+version: '8.x-1.0'
+core: '8.x'
+project: 'ms_ajax_form_example'
+datestamp: 1495974785
diff --git a/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.routing.yml b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.routing.yml
new file mode 100644
index 000000000..e453149aa
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.routing.yml
@@ -0,0 +1,7 @@
+ms_ajax_form_example.page:
+ path: '/multi-step-ajax-form'
+ defaults:
+ _form: '\Drupal\ms_ajax_form_example\Form\MultiStepExampleForm'
+ _title: 'Welcome'
+ requirements:
+ _permission: 'access content'
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/BaseButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/BaseButton.php
new file mode 100644
index 000000000..d6d195f79
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/BaseButton.php
@@ -0,0 +1,26 @@
+ 'submit',
+ '#value' => t('Next'),
+ '#goto_step' => StepsEnum::STEP_TWO,
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreeFinishButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreeFinishButton.php
new file mode 100644
index 000000000..3fe50884c
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreeFinishButton.php
@@ -0,0 +1,40 @@
+ 'submit',
+ '#value' => t('Finish!'),
+ '#goto_step' => StepsEnum::STEP_FINALIZE,
+ '#submit_handler' => 'submitValues',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSubmitHandler() {
+ return 'submitIntake';
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreePreviousButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreePreviousButton.php
new file mode 100644
index 000000000..58ab14429
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreePreviousButton.php
@@ -0,0 +1,33 @@
+ 'submit',
+ '#value' => t('Previous'),
+ '#goto_step' => StepsEnum::STEP_TWO,
+ '#skip_validation' => TRUE,
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoNextButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoNextButton.php
new file mode 100644
index 000000000..6289199de
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoNextButton.php
@@ -0,0 +1,32 @@
+ 'submit',
+ '#value' => t('Next'),
+ '#goto_step' => StepsEnum::STEP_THREE,
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoPreviousButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoPreviousButton.php
new file mode 100644
index 000000000..549a5c404
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoPreviousButton.php
@@ -0,0 +1,33 @@
+ 'submit',
+ '#value' => t('Previous'),
+ '#goto_step' => StepsEnum::STEP_ONE,
+ '#skip_validation' => TRUE,
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Form/MultiStepExampleForm.php b/sites/all/modules/examples/ms_ajax_form_example/src/Form/MultiStepExampleForm.php
new file mode 100644
index 000000000..147cc8fba
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Form/MultiStepExampleForm.php
@@ -0,0 +1,206 @@
+stepId = StepsEnum::STEP_ONE;
+ $this->stepManager = new StepManager();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'ms_ajax_form_example';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['wrapper-messages'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'id' => 'messages-wrapper',
+ ],
+ ];
+
+ $form['wrapper'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'id' => 'form-wrapper',
+ ],
+ ];
+
+ // Get step from step manager.
+ $this->step = $this->stepManager->getStep($this->stepId);
+
+ // Attach step form elements.
+ $form['wrapper'] += $this->step->buildStepFormElements();
+
+ // Attach buttons.
+ $form['wrapper']['actions']['#type'] = 'actions';
+ $buttons = $this->step->getButtons();
+ foreach ($buttons as $button) {
+ /** @var \Drupal\ms_ajax_form_example\Button\ButtonInterface $button */
+ $form['wrapper']['actions'][$button->getKey()] = $button->build();
+
+ if ($button->ajaxify()) {
+ // Add ajax to button.
+ $form['wrapper']['actions'][$button->getKey()]['#ajax'] = [
+ 'callback' => [$this, 'loadStep'],
+ 'wrapper' => 'form-wrapper',
+ 'effect' => 'fade',
+ ];
+ }
+
+ $callable = [$this, $button->getSubmitHandler()];
+ if ($button->getSubmitHandler() && is_callable($callable)) {
+ // Attach submit handler to button, so we can execute it later on..
+ $form['wrapper']['actions'][$button->getKey()]['#submit_handler'] = $button->getSubmitHandler();
+ }
+ }
+
+ return $form;
+
+ }
+
+ /**
+ * Ajax callback to load new step.
+ *
+ * @param array $form
+ * Form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state interface.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * Ajax response.
+ */
+ public function loadStep(array &$form, FormStateInterface $form_state) {
+ $response = new AjaxResponse();
+
+ $messages = drupal_get_messages();
+ if (!empty($messages)) {
+ // Form did not validate, get messages and render them.
+ $messages = [
+ '#theme' => 'status_messages',
+ '#message_list' => $messages,
+ '#status_headings' => [
+ 'status' => $this->t('Status message'),
+ 'error' => $this->t('Error message'),
+ 'warning' => $this->t('Warning message'),
+ ],
+ ];
+ $response->addCommand(new HtmlCommand('#messages-wrapper', $messages));
+ }
+ else {
+ // Remove messages.
+ $response->addCommand(new HtmlCommand('#messages-wrapper', ''));
+ }
+
+ // Update Form.
+ $response->addCommand(new HtmlCommand('#form-wrapper',
+ $form['wrapper']));
+
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ // Only validate if validation doesn't have to be skipped.
+ // For example on "previous" button.
+ if (empty($triggering_element['#skip_validation']) && $fields_validators = $this->step->getFieldsValidators()) {
+ // Validate fields.
+ foreach ($fields_validators as $field => $validators) {
+ // Validate all validators for field.
+ $field_value = $form_state->getValue($field);
+ foreach ($validators as $validator) {
+ if (!$validator->validates($field_value)) {
+ $form_state->setErrorByName($field, $validator->getErrorMessage());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Save filled values to step. So we can use them as default_value later on.
+ $values = [];
+ foreach ($this->step->getFieldNames() as $name) {
+ $values[$name] = $form_state->getValue($name);
+ }
+ $this->step->setValues($values);
+ // Add step to manager.
+ $this->stepManager->addStep($this->step);
+ // Set step to navigate to.
+ $triggering_element = $form_state->getTriggeringElement();
+ $this->stepId = $triggering_element['#goto_step'];
+
+ // If an extra submit handler is set, execute it.
+ // We already tested if it is callable before.
+ if (isset($triggering_element['#submit_handler'])) {
+ $this->{$triggering_element['#submit_handler']}($form, $form_state);
+ }
+
+ $form_state->setRebuild(TRUE);
+ }
+
+ /**
+ * Submit handler for last step of form.
+ *
+ * @param array $form
+ * Form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state interface.
+ */
+ public function submitValues(array &$form, FormStateInterface $form_state) {
+ // Submit all values to DB or do whatever you want on submit.
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Manager/StepManager.php b/sites/all/modules/examples/ms_ajax_form_example/src/Manager/StepManager.php
new file mode 100644
index 000000000..2d58d194f
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Manager/StepManager.php
@@ -0,0 +1,73 @@
+steps[$step->getStep()] = $step;
+ }
+
+ /**
+ * Fetches step from steps property, If it doesn't exist, create step object.
+ *
+ * @param int $step_id
+ * Step ID.
+ *
+ * @return \Drupal\ms_ajax_form_example\Step\StepInterface
+ * Return step object.
+ */
+ public function getStep($step_id) {
+ if (isset($this->steps[$step_id])) {
+ // If step was already initialized, use that step.
+ // Chance is there are values stored on that step.
+ $step = $this->steps[$step_id];
+ }
+ else {
+ // Get class.
+ $class = StepsEnum::map($step_id);
+ // Init step.
+ $step = new $class($this);
+ }
+
+ return $step;
+ }
+
+ /**
+ * Get all steps.
+ *
+ * @return \Drupal\ms_ajax_form_example\Step\StepInterface
+ * Steps.
+ */
+ public function getAllSteps() {
+ return $this->steps;
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/BaseStep.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/BaseStep.php
new file mode 100644
index 000000000..186926126
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/BaseStep.php
@@ -0,0 +1,80 @@
+step = $this->setStep();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStep() {
+ return $this->step;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLastStep() {
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValues($values) {
+ $this->values = $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues() {
+ return $this->values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldNames() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldsValidators() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ abstract protected function setStep();
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepFinalize.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepFinalize.php
new file mode 100644
index 000000000..c537e6cc3
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepFinalize.php
@@ -0,0 +1,38 @@
+ t('You have completed the wizard, yeah!'),
+ ];
+
+ return $form;
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepInterface.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepInterface.php
new file mode 100644
index 000000000..a19da3a08
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepInterface.php
@@ -0,0 +1,58 @@
+ 'textfield',
+ '#title' => t("What's your name?"),
+ '#required' => FALSE,
+ '#default_value' => isset($this->getValues()['name']) ? $this->getValues()['name'] : NULL,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldNames() {
+ return [
+ 'name',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldsValidators() {
+ return [
+ 'name' => [
+ new ValidatorRequired("Hey stranger, please tell me your name. I would like to get to know you."),
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepThree.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepThree.php
new file mode 100644
index 000000000..9d5771b66
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepThree.php
@@ -0,0 +1,70 @@
+ 'textfield',
+ '#title' => t('What is your LinkedIn URL?'),
+ '#default_value' => isset($this->getValues()['linkedin']) ? $this->getValues()['linkedin'] : NULL,
+ '#required' => FALSE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldNames() {
+ return [
+ 'linkedin',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldsValidators() {
+ return [
+ 'linkedin' => [
+ new ValidatorRequired("Tell me where I can find your LinkedIn please."),
+ new ValidatorRegex(t("I don't think this is a valid LinkedIn URL..."), '/(ftp|http|https):\/\/(.*)linkedin(.*)/'),
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepTwo.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepTwo.php
new file mode 100644
index 000000000..2fe1eb332
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepTwo.php
@@ -0,0 +1,68 @@
+ 'checkboxes',
+ '#title' => t('Nice to meet you! So, what are you interests?'),
+ '#options' => [1 => 'interest 1', 2 => 'interest 2', 3 => 'interest 3'],
+ '#default_value' => isset($this->getValues()['interests']) ? $this->getValues()['interests'] : [],
+ '#required' => FALSE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldNames() {
+ return [
+ 'interests',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldsValidators() {
+ return [
+ 'interests' => [
+ new ValidatorRequired("It would be a lot easier for me if you could fill out some of your interests."),
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepsEnum.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepsEnum.php
new file mode 100644
index 000000000..3c1d0288b
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepsEnum.php
@@ -0,0 +1,55 @@
+ 'step-one',
+ self::STEP_TWO => 'step-two',
+ self::STEP_THREE => 'step-three',
+ self::STEP_FINALIZE => 'step-finalize',
+ ];
+ }
+
+ /**
+ * Map steps to it's class.
+ *
+ * @param int $step
+ * Step number.
+ *
+ * @return bool
+ * Return true if exist.
+ */
+ public static function map($step) {
+ $map = [
+ self::STEP_ONE => 'Drupal\\ms_ajax_form_example\\Step\\StepOne',
+ self::STEP_TWO => 'Drupal\\ms_ajax_form_example\\Step\\StepTwo',
+ self::STEP_THREE => 'Drupal\\ms_ajax_form_example\\Step\\StepThree',
+ self::STEP_FINALIZE => 'Drupal\\ms_ajax_form_example\\Step\\StepFinalize',
+ ];
+
+ return isset($map[$step]) ? $map[$step] : FALSE;
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Validator/BaseValidator.php b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/BaseValidator.php
new file mode 100644
index 000000000..c7a663cdb
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/BaseValidator.php
@@ -0,0 +1,31 @@
+errorMessage = $error_message;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getErrorMessage() {
+ return $this->errorMessage;
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorInterface.php b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorInterface.php
new file mode 100644
index 000000000..d8512fe29
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorInterface.php
@@ -0,0 +1,22 @@
+pattern = $pattern;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validates($value) {
+ return preg_match($this->pattern, $value);
+ }
+
+}
diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRequired.php b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRequired.php
new file mode 100644
index 000000000..ac0b2ab54
--- /dev/null
+++ b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRequired.php
@@ -0,0 +1,19 @@
+