removed examples contrib module

This commit is contained in:
2024-09-17 11:59:06 +02:00
parent 5a8b199fa2
commit 8bd90a8a02
396 changed files with 0 additions and 26299 deletions
@@ -1,95 +0,0 @@
{
"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
}]
}
}
@@ -1,63 +0,0 @@
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.
@@ -1,339 +0,0 @@
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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.
<signature of Ty Coon>, 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.
@@ -1,79 +0,0 @@
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.
@@ -1,86 +0,0 @@
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 .
@@ -1,114 +0,0 @@
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.
@@ -1,14 +0,0 @@
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
@@ -1,7 +0,0 @@
ajax_example.library:
version: 1.x
css:
base:
css/ajax-example-base.css: {}
js:
js/ajax-example.js: {}
@@ -1,72 +0,0 @@
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
@@ -1,23 +0,0 @@
<?php
/**
* @file
* AJAX Examples module file with basic examples.
*/
/**
* @defgroup ajax_example Example: AJAX
* @ingroup examples
* @{
* These examples show basic AJAX concepts.
*
* General documentation is available at
* @link ajax AJAX Framework documentation @endlink and at the
* @link http://drupal.org/node/752056 AJAX Forms handbook page @endlink.
*
* The several examples here demonstrate basic AJAX usage.
*/
/**
* @} End of "defgroup ajax_example".
*/
@@ -1,96 +0,0 @@
ajax_example.description:
path: 'examples/ajax-example'
defaults:
_controller: '\Drupal\ajax_example\Controller\AjaxExampleController::description'
_title: 'AJAX Example'
requirements:
_permission: 'access content'
ajax_example.simplest:
path: 'examples/ajax-example/simplest'
defaults:
_form: '\Drupal\ajax_example\Form\Simplest'
_title: 'Simplest AJAX example'
requirements:
_permission: 'access content'
ajax_example.autotextfields:
path: 'examples/ajax-example/autotextfields'
defaults:
_form: '\Drupal\ajax_example\Form\Autotextfields'
_title: 'Generate textfields'
requirements:
_permission: 'access content'
ajax_example.submit_driven_ajax:
path: 'examples/ajax-example/submit-driven-ajax'
defaults:
_form: '\Drupal\ajax_example\Form\SubmitDriven'
_title: 'Submit-driven AJAX'
requirements:
_permission: 'access content'
ajax_example.dependent_dropdown:
path: 'examples/ajax-example/dependent-dropdown/{nojs}'
defaults:
_form: '\Drupal\ajax_example\Form\DependentDropdown'
_title: 'Dependent dropdown'
nojs: ajax
requirements:
_permission: 'access content'
ajax_example.dynamic_form_sections:
path: 'examples/ajax-example/dynamic-form-sections/{nojs}'
defaults:
_form: '\Drupal\ajax_example\Form\DynamicFormSections'
_title: 'Dynamic form sections'
nojs: 'ajax'
requirements:
_permission: 'access content'
ajax_example.wizard:
path: 'examples/ajax-example/wizard'
defaults:
_form: '\Drupal\ajax_example\Form\Wizard'
_title: 'Wizard with graceful degradation'
requirements:
_permission: 'access content'
ajax_example.wizardnojs:
path: 'examples/ajax-example/wizard-nojs/{no_js_use}'
defaults:
_form: '\Drupal\ajax_example\Form\Wizard'
_title: 'Wizard with graceful degradation, w/JS turned off'
no_js_use: TRUE
requirements:
_permission: 'access content'
ajax_example.ajax_link_render:
path: 'examples/ajax-example/ajax-link-renderable'
defaults:
_controller: '\Drupal\ajax_example\Controller\AjaxExampleController::renderLinkRenderableArray'
_title: 'AJAX link from a render array'
requirements:
_permission: 'access content'
# This route is for an AJAX callback. It is used by the AJAX system on
# ajax_example.ajax_link_render. It has a {nojs} parameter, which gives us
# a way to know whether the request is an AJAX request or is from some other
# source.
ajax_example.ajax_link_callback:
path: 'examples/ajax-example/ajax-link-callback/{nojs}'
defaults:
_controller: '\Drupal\ajax_example\Controller\AjaxExampleController::ajaxLinkCallback'
# We provide a default value for {nojs}, so that it can be an optional
# parameter.
nojs: 'nojs'
requirements:
_permission: 'access content'
ajax_example.autocomplete_user:
path: 'examples/ajax_example/user_autocomplete'
defaults:
_form: '\Drupal\ajax_example\Form\EntityAutocomplete'
_title: 'Autocomplete users with entity_autocomplete.'
requirements:
_permission: 'access content'
@@ -1,9 +0,0 @@
/*
* @file
* CSS for ajax_example.
*/
/* Put some buttons inline, for nicer formatting. */
.ajax-example-inline {
display: inline-block;
}
@@ -1,19 +0,0 @@
/**
* @file
* JavaScript for ajax_example.
*/
(function ($) {
// Re-enable form elements that are disabled for non-ajax situations.
Drupal.behaviors.enableFormItemsForAjaxForms = {
attach: function () {
// If ajax is enabled, we want to hide items that are marked as hidden in
// our example.
if (Drupal.ajax) {
$('.ajax-example-hide').hide();
}
}
};
})(jQuery);
@@ -1,112 +0,0 @@
<?php
namespace Drupal\ajax_example\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\examples\Utility\DescriptionTemplateTrait;
use Symfony\Component\HttpFoundation\Response;
/**
* Controller routines for AJAX example routes.
*/
class AjaxExampleController extends ControllerBase {
use DescriptionTemplateTrait;
/**
* {@inheritdoc}
*/
protected function getModuleName() {
return 'ajax_example';
}
/**
* Demonstrates a clickable AJAX-enabled link using the 'use-ajax' class.
*
* Because of the 'use-ajax' class applied here, the link submission is done
* without a page refresh.
*
* When using the AJAX framework outside the context of a form or a renderable
* array of type 'link', you have to include ajax.js explicitly.
*
* @return array
* Form API array.
*
* @ingroup ajax_example
*/
public function renderLinkRenderableArray() {
$build['my_div'] = [
'#markup' => $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;
}
}
@@ -1,124 +0,0 @@
<?php
namespace Drupal\ajax_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Show textfields based on AJAX-enabled checkbox clicks.
*
* @ingroup ajax_example
*/
class Autotextfields extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_example_autotextfields';
}
/**
* {@inheritdoc}
*
* This form has two checkboxes which the user can check in order to then
* reveal the first and/or last name text fields.
*
* We could perform this behavior with #states. We might not want to if, for
* instance, we wanted to require a name, but let the user choose whether
* to enter first or last or both.
*
* For all the requests this class gets, the buildForm() method will always be
* called. If an AJAX request comes in, the form state will be set to the
* state the user changed that caused the AJAX request. So if the user enabled
* one of our checkboxes, it will be checked in $form_state.
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['description'] = [
'#type' => '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'];
}
}
@@ -1,269 +0,0 @@
<?php
namespace Drupal\ajax_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
/**
* Re-populate a dropdown based on form state.
*/
class DependentDropdown extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_example_dependentdropdown';
}
/**
* {@inheritdoc}
*
* The $nojs parameter is specified as a path parameter on the route.
*
* @see ajax_example.routing.yml
*/
public function buildForm(array $form, FormStateInterface $form_state, $nojs = NULL) {
// Add our CSS and tiny JS to hide things when they should be hidden.
$form['#attached']['library'][] = 'ajax_example/ajax_example.library';
// Explanatory text with helpful links.
$form['info'] = [
'#markup' =>
$this->t('<p>Like other examples in this module, this form has a path that
can be modified with /nojs to simulate its behavior without JavaScript.
</p><ul>
<li>@try_it_without_ajax</li>
<li>@try_it_with_ajax</li>
</ul>',
[
'@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;
}
}
@@ -1,222 +0,0 @@
<?php
namespace Drupal\ajax_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
/**
* Dynamically-enabled form with graceful no-JS degradation.
*
* Example of a form with portions dynamically enabled or disabled, but with
* graceful degradation in the case of no JavaScript.
*
* The idea here is that certain parts of the form don't need to be displayed
* unless a given option is selected, but then they should be displayed and
* configured.
*
* The third $no_js_use argument is strictly for demonstrating operation
* without JavaScript, without making the user/developer turn off JavaScript.
*/
class DynamicFormSections extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_example_dynamicsectiondegrades';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $nojs = NULL) {
// Add our CSS and tiny JS to hide things when they should be hidden.
$form['#attached']['library'][] = 'ajax_example/ajax_example.library';
// Explanatory text with helpful links.
$form['info'] = [
'#markup' =>
$this->t('<p>Like other examples in this module, this form has a path that
can be modified with /nojs to simulate its behavior without JavaScript.
</p><ul>
<li>@try_it_without_ajax</li>
<li>@try_it_with_ajax</li>
</ul>',
[
'@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'];
}
}
@@ -1,126 +0,0 @@
<?php
namespace Drupal\ajax_example\Form;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A simple autocomplete form which looks up usernames.
*
* @ingroup ajax_example
*/
class EntityAutocomplete implements FormInterface, ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity type manager service.
*
* We need this for the submit handler.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Container injection factory.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The service discovery container.
*
* @return self
* The form object.
*/
public static function create(ContainerInterface $container) {
$form = new static(
$container->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' => '<div>' . 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.") . '</div>',
];
// 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));
}
}
@@ -1,80 +0,0 @@
<?php
namespace Drupal\ajax_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* A relatively simple AJAX demonstration form.
*/
class Simplest extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_example_simplest';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['changethis'] = [
'#title' => $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'];
}
}
@@ -1,73 +0,0 @@
<?php
namespace Drupal\ajax_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Submit a form without a page reload.
*/
class SubmitDriven extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_example_autotextfields';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// This container wil be replaced by AJAX.
$form['container'] = [
'#type' => 'container',
'#attributes' => ['id' => 'box-container'],
];
// The box contains some markup that we can change on a submit request.
$form['container']['box'] = [
'#type' => 'markup',
'#markup' => '<h1>Initial markup for box</h1>',
];
$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;
}
}
@@ -1,229 +0,0 @@
<?php
namespace Drupal\ajax_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
/**
* AJAX example wizard.
*/
class Wizard extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'ajax_example_wizard';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $no_js_use = FALSE) {
$url = Url::fromUri('internal:/examples/ajax-example/wizard-nojs');
$link = Link::fromTextAndUrl($this->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;
}
}
}
@@ -1,29 +0,0 @@
{#
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 %}
<p>The AJAX example module provides many examples of AJAX including forms, links, and AJAX commands.</p>
<p><a href={{ simple_ajax_example }}>Simplest AJAX Example</a></p>
<p><a href={{ ajax_generate_textfields }}>Generate textfields</a></p>
<p><a href={{ ajax_submit }}>Submit-driven AJAX</a></p>
<p><a href={{ ajax_dependent_dropdown }}>Dependent dropdown</a></p>
<p><a href={{ ajax_dependent_dropdown_nojs }}>Dependent dropdown w/ no JS</a></p>
<p><a href={{ ajax_dynamic_form }}>Dynamic form sections</a></p>
<p><a href={{ ajax_dynamic_form_nojs }}>Dynamic form sections w/ no JS</a></p>
<p><a href={{ ajax_wizard_example }}>AJAX Wizard Example</a></p>
<p><a href={{ ajax_wizard_example_nojs }}>AJAX Wizard Example w/JS turned off</a></p>
{% endtrans %}
@@ -1,79 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Verify tool block menu items and operability of all our routes.
*
* @group ajax_example
* @group examples
*
* @ingroup ajax_example
*/
class AjaxExampleMenuTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* The installation profile to use with this test.
*
* @var string
*/
protected $profile = 'minimal';
/**
* Tests links.
*/
public function testAjaxExampleLinks() {
// Login a user that can access content.
$this->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);
}
}
}
}
@@ -1,74 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Functional test of non-AJAX dependent dropdown example.
*
* @group ajax_example
* @group examples
*
* @ingroup ajax_example
*
* @see \Drupal\Tests\ajax_example\FunctionalJavascript\DependentDropdownTest
*/
class DependentDropdownTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the dependent dropdown form without AJAX.
*/
public function testDependentDropdown() {
// Get the Mink stuff.
$session = $this->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");
}
}
}
}
@@ -1,70 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Functional test of non-AJAX dependent dropdown example.
*
* @group ajax_example
* @group examples
*
* @ingroup ajax_example
*
* @see Drupal\Tests\ajax_example\FunctionalJavascript\DynamicFormSectionsTest
*/
class DynamicFormSectionsTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the dynamic sections form without AJAX.
*/
public function testDynamicFormSections() {
// Get the Mink stuff.
$assert = $this->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');
}
}
@@ -1,80 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Test the user interactions for the Autotextfields example.
*
* @group ajax_example
*/
class AutofieldsTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the user interactions for the Autotextfields example.
*/
public function testAutotextfields() {
// Get our Mink stuff.
$session = $this->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');
}
}
@@ -1,75 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Functional test of dependent dropdown example.
*
* @group ajax_example
*
* @see \Drupal\Tests\ajax_example\FunctionalJavascript\DependentDropdownTest
*/
class DependentDropdownTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the dependent dropdown form with AJAX.
*/
public function testDependentDropdown() {
// Get the Mink stuff.
$assert = $this->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");
}
}
}
}
@@ -1,67 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Functional test of dependent dropdown example.
*
* @group ajax_example
*
* @see Drupal\Tests\ajax_example\Functional\DynamicFormSectionsTest
*/
class DynamicFormSectionsTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the dependent dropdown form with AJAX.
*/
public function testDynamicFormSections() {
// Get the Mink stuff.
$assert = $this->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');
}
}
}
@@ -1,69 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests the behavior of the entity_autocomplete example.
*
* @group ajax_example
*/
class EntityAutocompleteTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the behavior of the submit-driven AJAX example.
*
* Behaviors to test:
* - GET the route ajax_example.autocomplete_user.
* - Examine the DOM to make sure our change hasn't happened yet.
* - Send an event to the DOM to trigger the autocomplete.
* - Wait for the autocomplete request to complete.
* - Examine the DOM to see if our expected change happened.
* - Submit some names to see if our form processed the user properly.
*/
public function testSubmitDriven() {
// Set up some accounts with known names.
$names = ['bb', 'bc'];
foreach ($names as $name) {
$this->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');
}
}
@@ -1,60 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Test the user interactions for the Simplest example.
*
* @group ajax_example
*/
class SimplestTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the user interactions for the Autotextfields example.
*/
public function testAutotextfields() {
// Get our Mink stuff.
$session = $this->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()
);
}
}
}
@@ -1,45 +0,0 @@
<?php
namespace Drupal\Tests\ajax_example\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests the behavior of the submit-driven AJAX example.
*
* @group ajax_example
*/
class SubmitDrivenTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['ajax_example'];
/**
* Test the behavior of the submit-driven AJAX example.
*
* Behaviors to test:
* - GET the route ajax_example.submit_driven_ajax.
* - Examine the DOM to make sure our change hasn't happened yet.
* - Submit the form.
* - Wait for the AJAX request to complete.
* - Examine the DOM to see if our expected change happened.
*/
public function testSubmitDriven() {
// Get the session assertion object.
$assert = $this->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):');
}
}
@@ -1,14 +0,0 @@
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
@@ -1,83 +0,0 @@
<?php
/**
* @file
* Install, update, and uninstall functions for the batch_example module.
*/
/**
* Example of batch-driven update function.
*
* Because some update functions may require the batch API, the $sandbox
* provides a place to store state. When $sandbox['#finished'] == TRUE,
* calls to this update function are completed.
*
* The $sandbox param provides a way to store data during multiple invocations.
* When the $sandbox['#finished'] == 1, execution is complete.
*
* This dummy 'update' function changes no state in the system. It simply
* loads each node.
*
* To make this update function run again and again, execute the query
* "update system set schema_version = 0 where name = 'batch_example';"
* and then run /update.php.
*
* @ingroup batch_example
*/
function batch_example_update_8001(&$sandbox) {
// Use the sandbox at your convenience to store the information needed
// to track progression between successive calls to the function.
if (!isset($sandbox['progress'])) {
// The count of nodes visited so far.
$sandbox['progress'] = 0;
// Total nodes that must be visited.
$sandbox['max'] = db_query('SELECT COUNT(nid) FROM {node}')->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 = '<ul><li>' . implode('</li><li>', $sandbox['messages']) . "</li></ul>";
return t('The batch_example demonstration update did what it was supposed to do: @message', ['@message' => $final_message]);
}
}
@@ -1,5 +0,0 @@
# 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
@@ -1,133 +0,0 @@
<?php
/**
* @file
* Outlines how a module can use the Batch API.
*/
/**
* @defgroup batch_example Example: Batch API
* @ingroup examples
* @{
* Outlines how a module can use the Batch API.
*
* Batches allow heavy processing to be spread out over several page
* requests, ensuring that the processing does not get interrupted
* because of a PHP timeout, while allowing the user to receive feedback
* on the progress of the ongoing operations. It also can reduce out of memory
* situations.
*
* The @link batch_example.install .install file @endlink also shows how the
* Batch API can be used to handle long-running hook_update_N() functions.
*
* Two harmless batches are defined:
* - batch 1: Load the node with the lowest nid 100 times.
* - batch 2: Load all nodes, 20 times and uses a progressive op, loading nodes
* by groups of 5.
*
* @see batch
*/
/**
* Batch operation for batch 1: one at a time.
*
* This is the function that is called on each operation in batch 1.
*/
function batch_example_op_1($id, $operation_details, &$context) {
// Simulate long process by waiting 1/50th of a second.
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'][] = $id;
// Optional message displayed under the progressbar.
$context['message'] = t('Running Batch "@id" @details',
['@id' => $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".
*/
@@ -1,7 +0,0 @@
batch_example.form:
path: 'examples/batch_example'
defaults:
_form: '\Drupal\batch_example\Form\BatchExampleForm'
_title: 'Demo of batch processing'
requirements:
_permission: 'access content'
@@ -1,148 +0,0 @@
<?php
namespace Drupal\batch_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form with examples on how to use cache.
*/
class BatchExampleForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'batch_example_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['description'] = [
'#type' => '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;
}
}
@@ -1,40 +0,0 @@
<?php
namespace Drupal\Tests\batch_example\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Functional tests for the Batch Example module.
*
* @group fapi_example
*
* @ingroup batch_example
*/
class BatchExampleWebTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
static public $modules = ['node', 'batch_example'];
/**
* Login user and test both batch examples.
*/
public function testBatchExampleBasic() {
// Login the admin user.
$web_user = $this->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');
}
}
@@ -1,15 +0,0 @@
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
@@ -1,3 +0,0 @@
block_example.description:
title: Block Example
route_name: block_example.description
@@ -1,53 +0,0 @@
<?php
/**
* @file
* Module file for block_example.
*/
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Block\BlockPluginInterface;
/**
* @defgroup block_example Example: Block
* @ingroup examples
* @{
* Demonstrates code creation of blocks.
*
* This example demonstrates how a module can define blocks that can be
* displayed on various pages of a site, and how to alter blocks provided by
* other modules.
*/
/**
* Implements hook_block_view_alter().
*
* This hook allows you to modify the output of any block in the system.
*
* We are going to change the block label to uppercase if it contains the string
* "uppercase" or if the default block label contains this string. The default
* block label is set programmatically in the subject key of the block's plugin
* definition. The configurable block label, which can be overridden through the
* UI, is found in the "#configuration" key of the block's build definition.
* This module creates a block that demonstrates the uppercase effect in the
* "Example: uppercase this please" block. You can also demonstrate the effect
* of this hook by editing the title of an existing block or by creating a new
* block which where the default label has the string "uppercase" in it.
*
* Instead of hook_block_view_alter(), which is called for all blocks, you can
* also use hook_block_view_BASE_BLOCK_ID_alter() to alter a specific block. To
* only change the "example_uppercase" block we would use the function:
* hook_block_view_example_uppercase_alter().
*/
function block_example_block_view_alter(array &$build, BlockPluginInterface $block) {
// We'll search for the string 'uppercase'.
$definition = $block->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".
*/
@@ -1,7 +0,0 @@
block_example.description:
path: '/examples/block-example'
defaults:
_controller: '\Drupal\block_example\Controller\BlockExampleController::description'
_title: 'Block Example'
requirements:
_permission: 'access content'
@@ -1,7 +0,0 @@
block.settings.example_configurable_text:
type: block_settings
label: 'Example configurable text block configuration'
mapping:
block_example_string:
type: text
label: 'Block contents'
@@ -1,19 +0,0 @@
<?php
namespace Drupal\block_example\Controller;
use Drupal\examples\Utility\DescriptionTemplateTrait;
/**
* Controller routines for block example routes.
*/
class BlockExampleController {
use DescriptionTemplateTrait;
/**
* {@inheritdoc}
*/
protected function getModuleName() {
return 'block_example';
}
}
@@ -1,82 +0,0 @@
<?php
namespace Drupal\block_example\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a 'Example: configurable text string' block.
*
* Drupal\Core\Block\BlockBase gives us a very useful set of basic functionality
* for this configurable block. We can just fill in a few of the blanks with
* defaultConfiguration(), blockForm(), blockSubmit(), and build().
*
* @Block(
* id = "example_configurable_text",
* admin_label = @Translation("Example: configurable text")
* )
*/
class ExampleConfigurableTextBlock extends BlockBase {
/**
* {@inheritdoc}
*
* This method sets the block default configuration. This configuration
* determines the block's behavior when a block is initially placed in a
* region. Default values for the block configuration form should be added to
* the configuration array. System default configurations are assembled in
* BlockBase::__construct() e.g. cache setting and block title visibility.
*
* @see \Drupal\block\BlockBase::__construct()
*/
public function defaultConfiguration() {
return [
'block_example_string' => $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'],
];
}
}
@@ -1,30 +0,0 @@
<?php
namespace Drupal\block_example\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Example: empty block' block.
*
* @Block(
* id = "example_empty",
* admin_label = @Translation("Example: empty block")
* )
*/
class ExampleEmptyBlock extends BlockBase {
/**
* {@inheritdoc}
*
* The return value of the build() method is a renderable array. Returning an
* empty array will result in empty block contents. The front end will not
* display empty blocks.
*/
public function build() {
// We return an empty array on purpose. The block will thus not be rendered
// on the site. See BlockExampleTest::testBlockExampleBasic().
return [];
}
}
@@ -1,26 +0,0 @@
<?php
namespace Drupal\block_example\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Example: uppercase this please' block.
*
* @Block(
* id = "example_uppercase",
* admin_label = @Translation("Example: uppercase this please")
* )
*/
class ExampleUppercaseBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => t("This block's title is changed to uppercase. Any block title which contains 'uppercase' will also be changed to uppercase."),
];
}
}
@@ -1,15 +0,0 @@
{#
Description text for the Block Example.
#}
{% set block_admin_page = url('block.admin_display')['#markup'] %}
{% trans %}
<p>The Block Example provides three sample blocks which demonstrate the various
block APIs. To experiment with the blocks, enable and configure them on
<a href="{{ block_admin_page }}">the block admin page</a>.</p>
{% endtrans %}
@@ -1,50 +0,0 @@
<?php
namespace Drupal\Tests\block_example\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Test the user-facing menus in Block Example.
*
* @ingroup block_example
*
* @group block_example
* @group examples
*/
class BlockExampleMenuTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'block_example'];
/**
* The installation profile to use with this test.
*
* This test class requires the "Tools" block.
*
* @var string
*/
protected $profile = 'minimal';
/**
* Test for a link to the block example in the Tools menu.
*/
public function testBlockExampleLink() {
$this->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);
}
}
@@ -1,87 +0,0 @@
<?php
namespace Drupal\Tests\block_example\Functional;
use Drupal\Component\Utility\Unicode;
use Drupal\Tests\BrowserTestBase;
/**
* Test the configuration options and block created by Block Example module.
*
* @ingroup block_example
*
* @group block_example
* @group examples
*/
class BlockExampleTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'block_example'];
/**
* Tests block_example functionality.
*/
public function testBlockExampleBasic() {
$assert = $this->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]']);
}
}
@@ -1,14 +0,0 @@
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
@@ -1,4 +0,0 @@
cache_example.description:
title: 'Cache Example'
description: 'Example of Drupal Cache API'
route_name: cache_example.description
@@ -1,36 +0,0 @@
<?php
/**
* @file
* Outlines how a module can use the Cache API.
*
* @todo: Demonstrate allowing invalid cache items.
* @todo: Demonstrate deleteing cache entries.
* @todo: Demonstrate deleteing cache tags.
* @todo: Demonstrate deleteing all cache items.
* @todo: Demonstrate invalidating cache entries.
* @todo: Demonstrate invalidating cache tags.
* @todo: Demonstrate invalidating all cache items.
*/
/**
* @defgroup cache_example Example: Cache API
* @ingroup examples
* @{
* Outlines how a module can use the Cache API.
*
* Cache API allows us to cache data that is heavy to calculate. As this can
* significantly speed up the Drupal site, it is recommended to use cache
* mechanism when it is appropriate.
*
* Cache in Drupal is very easy to use. This example will search entire Drupal
* folder and display all files. Since this operation includes filesystem it can
* take a while. This list will not change much on production
* websites, so we decide to cache it.
*
* @see \Drupal\Core\Cache\CacheBackendInterface
*/
/**
* @} End of "defgroup cache_example".
*/
@@ -1,6 +0,0 @@
cache_example.description:
path: 'examples/cache-example'
defaults:
_form: '\Drupal\cache_example\Form\CacheExampleForm'
requirements:
_permission: 'access content'
@@ -1,306 +0,0 @@
<?php
namespace Drupal\cache_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Form with examples on how to use cache.
*/
class CacheExampleForm extends FormBase {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The cache.default cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* Dependency injection through the constructor.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The string translation service.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current active user service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache object associated with the default bin.
*/
public function __construct(
RequestStack $request_stack,
TranslationInterface $translation,
AccountProxyInterface $current_user,
CacheBackendInterface $cache_backend
) {
$this->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 = '<p>' . $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.') . '</p>';
$intro_message .= '<p>'
. $this->t(
'<a href="@url">Reload this page</a> to see cache in action.',
['@url' => $this->getRequest()->getRequestUri()]
)
. ' ';
$intro_message .= $this->t('You can use the button below to remove cached data.') . '</p>';
$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. <br/>(Source: <span style="color:@color;">@source</span>)', [
'%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) {
}
}
@@ -1,110 +0,0 @@
<?php
namespace Drupal\Tests\cache_example\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests for the cache_example module.
*
* @ingroup cache_example
*
* @group cache_example
* @group examples
*/
class CacheExampleTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['cache_example'];
/**
* The installation profile to use with this test.
*
* @var string
*/
protected $profile = 'minimal';
/**
* Test menu links and routes.
*
* Test the following:
* - A link to the cache_example in the Tools menu.
* - That you can successfully access the cache_example form.
*/
public function testCacheExampleMenu() {
$assert = $this->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');
}
}
@@ -1,12 +0,0 @@
{
"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+"
}
@@ -1,20 +0,0 @@
# 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'
@@ -1,28 +0,0 @@
# 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'
@@ -1,13 +0,0 @@
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
@@ -1,14 +0,0 @@
# 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
@@ -1,3 +0,0 @@
config_entity_example.menu:
title: Config Entity Example
route_name: entity.robot.list
@@ -1,42 +0,0 @@
<?php
/**
* @file
* Demonstrates how to define a new configuration entity type.
*/
/**
* @defgroup config_entity_example Example: Config Entity
* @ingroup examples
* @{
* Implement a Config Entity.
*
* This module demonstrates implementing a Config Entity.
*
* This is an example of a simple configuration entity, the kind you might
* create to store administrator-defined objects like blocks or views.
*
* In this module we define a configuration entity named 'Robot'. The entity
* has a unique ID (also called a machine name), a human-readable label used
* for display, and a universally unique identifier. You can create new robots
* by navigating to &lt;your_site_root&gt;/examples/config_entity_example. A
* default robot, "marvin", is included with the module.
*
* What's special about a Configuration Entity?
*
* Configuration entities are entities just like content entities. The key
* difference is where the data is stored. Content entities are stored in the
* database. Configuration entities are stored in *.yml files, typically under
* &lt;your_site_root&gt;/sites/default/files/config_&lt;unique_id&gt;.
*
* Another key difference with configuration entities is the expectation they
* are created by administrators, and not end users. As files, configuration
* entities can be added to a version control system.
*
* Originally based on code from blog post at
* http://previousnext.com.au/blog/understanding-drupal-8s-config-entities
*/
/**
* @} End of "defgroup config_entity_example".
*/
@@ -1,4 +0,0 @@
#Define the administer permission for the robot config entity.
'administer robots':
title: 'Administer robots'
description: Create and edit robots.
@@ -1,56 +0,0 @@
# The routing.yml file defines the paths for our module.
# Here we define the paths for our entity type's admin UI.
# This is the router item for listing all entities.
entity.robot.list:
path: '/examples/config-entity-example'
defaults:
# '_entity_list' tells Drupal to use an entity list controller.
# We give the entity ID here. Drupal then looks in the entity's annotation
# and looks for the "list" entry under "controllers" for the class to load.
# @see \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer
_entity_list: 'robot'
_title: 'Config Entity Example'
requirements:
_permission: 'administer robots'
# This is the router item for adding our entity.
entity.robot.add_form:
path: '/examples/config-entity-example/add'
defaults:
_title: 'Add robot'
# Like _entity_list above, _entity_form gives the entity type ID, only this
# time also lists the form separated by a period. Drupal looks in the
# annotation for the entity and locates the "add" entry under "form" for
# the form class to load.
# @see \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer
_entity_form: robot.add
requirements:
_entity_create_access: robot
# This is the router item for editing our entity.
entity.robot.edit_form:
# Parameters may be passed to the form via the URL path. We name the
# parameter in the path by enclosing it in curly braces. For entity forms,
# we include the entity ID in the path by including a parameter with the
# same name as the entity type ID.
path: '/examples/config-entity-example/manage/{robot}'
defaults:
_title: 'Edit robot'
# List our add entry above, this _entity_form entry instructs Drupal to
# read our entity type's annonation, and look for the "edit" entry under
# "form".
_entity_form: robot.edit
requirements:
# This uses our entity access controller.
# @see \Drupal\Core\Entity\EntityAccessCheck
_entity_access: robot.update
# This is the router item for deleting an instance of our entity.
entity.robot.delete_form:
path: '/examples/config-entity-example/manage/{robot}/delete'
defaults:
_title: 'Delete robot'
_entity_form: robot.delete
requirements:
_entity_access: robot.delete
@@ -1,87 +0,0 @@
<?php
namespace Drupal\config_entity_example\Controller;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\examples\Utility\DescriptionTemplateTrait;
/**
* Provides a listing of robot entities.
*
* List Controllers provide a list of entities in a tabular form. The base
* class provides most of the rendering logic for us. The key functions
* we need to override are buildHeader() and buildRow(). These control what
* columns are displayed in the table, and how each row is displayed
* respectively.
*
* Drupal locates the list controller by looking for the "list" entry under
* "controllers" in our entity type's annotation. We define the path on which
* the list may be accessed in our module's *.routing.yml file. The key entry
* to look for is "_entity_list". In *.routing.yml, "_entity_list" specifies
* an entity type ID. When a user navigates to the URL for that router item,
* Drupal loads the annotation for that entity type. It looks for the "list"
* entry under "controllers" for the class to load.
*
* @ingroup config_entity_example
*/
class RobotListBuilder extends ConfigEntityListBuilder {
use DescriptionTemplateTrait;
/**
* {@inheritdoc}
*/
protected function getModuleName() {
return 'config_entity_example';
}
/**
* Builds the header row for the entity listing.
*
* @return array
* A render array structure of header strings.
*
* @see \Drupal\Core\Entity\EntityListController::render()
*/
public function buildHeader() {
$header['label'] = $this->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;
}
}
@@ -1,89 +0,0 @@
<?php
namespace Drupal\config_entity_example\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
/**
* Defines the robot entity.
*
* The lines below, starting with '@ConfigEntityType,' are a plugin annotation.
* These define the entity type to the entity type manager.
*
* The properties in the annotation are as follows:
* - id: The machine name of the entity type.
* - label: The human-readable label of the entity type. We pass this through
* the "@Translation" wrapper so that the multilingual system may
* translate it in the user interface.
* - handlers: An array of entity handler classes, keyed by handler type.
* - access: The class that is used for access checks.
* - list_builder: The class that provides listings of the entity.
* - form: An array of entity form classes keyed by their operation.
* - entity_keys: Specifies the class properties in which unique keys are
* stored for this entity type. Unique keys are properties which you know
* will be unique, and which the entity manager can use as unique in database
* queries.
* - links: entity URL definitions. These are mostly used for Field UI.
* Arbitrary keys can set here. For example, User sets cancel-form, while
* Node uses delete-form.
*
* @see http://previousnext.com.au/blog/understanding-drupal-8s-config-entities
* @see annotation
* @see Drupal\Core\Annotation\Translation
*
* @ingroup config_entity_example
*
* @ConfigEntityType(
* id = "robot",
* label = @Translation("Robot"),
* admin_permission = "administer robots",
* handlers = {
* "access" = "Drupal\config_entity_example\RobotAccessController",
* "list_builder" = "Drupal\config_entity_example\Controller\RobotListBuilder",
* "form" = {
* "add" = "Drupal\config_entity_example\Form\RobotAddForm",
* "edit" = "Drupal\config_entity_example\Form\RobotEditForm",
* "delete" = "Drupal\config_entity_example\Form\RobotDeleteForm"
* }
* },
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* links = {
* "edit-form" = "/examples/config_entity_example/manage/{robot}",
* "delete-form" = "/examples/config_entity_example/manage/{robot}/delete"
* }
* )
*/
class Robot extends ConfigEntityBase {
/**
* The robot ID.
*
* @var string
*/
public $id;
/**
* The robot UUID.
*
* @var string
*/
public $uuid;
/**
* The robot label.
*
* @var string
*/
public $label;
/**
* The robot floopy flag.
*
* @var string
*/
public $floopy;
}
@@ -1,35 +0,0 @@
<?php
namespace Drupal\config_entity_example\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Class RobotAddForm.
*
* Provides the add form for our Robot entity.
*
* @ingroup config_entity_example
*/
class RobotAddForm extends RobotFormBase {
/**
* Returns the actions provided by this form.
*
* For our add form, we only need to change the text of the submit button.
*
* @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) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Create Robot');
return $actions;
}
}
@@ -1,89 +0,0 @@
<?php
namespace Drupal\config_entity_example\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
/**
* Class RobotDeleteForm.
*
* Provides a confirm form for deleting the entity. This is different from the
* add and edit forms as it does not inherit from RobotFormBase. The reason for
* this is that we do not need to build the same form. Instead, we present the
* user with a simple yes/no question. For this reason, we derive from
* EntityConfirmFormBase instead.
*
* @ingroup config_entity_example
*/
class RobotDeleteForm extends EntityConfirmFormBase {
/**
* Gathers a confirmation question.
*
* The question is used as a title in our confirm form. For delete confirm
* forms, this typically takes the form of "Are you sure you want to
* delete...", including the entity label.
*
* @return string
* Translated string.
*/
public function getQuestion() {
return $this->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());
}
}
@@ -1,35 +0,0 @@
<?php
namespace Drupal\config_entity_example\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Class RobotEditForm.
*
* Provides the edit form for our Robot entity.
*
* @ingroup config_entity_example
*/
class RobotEditForm extends RobotFormBase {
/**
* Returns the actions provided by this form.
*
* For the edit form, we only need to change the text of the submit button.
*
* @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) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Update Robot');
return $actions;
}
}
@@ -1,221 +0,0 @@
<?php
namespace Drupal\config_entity_example\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class RobotFormBase.
*
* Typically, we need to build the same form for both adding a new entity,
* and editing an existing entity. Instead of duplicating our form code,
* we create a base class. Drupal never routes to this class directly,
* but instead through the child classes of RobotAddForm and RobotEditForm.
*
* @ingroup config_entity_example
*/
class RobotFormBase extends EntityForm {
/**
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $entityQueryFactory;
/**
* Construct the RobotFormBase.
*
* For simple entity forms, there's no need for a constructor. Our robot form
* base, however, requires an entity query factory to be injected into it
* from the container. We later use this query factory to build an entity
* query for the exists() method.
*
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
* An entity query factory for the robot entity type.
*/
public function __construct(QueryFactory $query_factory) {
$this->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');
}
}
@@ -1,34 +0,0 @@
<?php
namespace Drupal\config_entity_example;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an access controller for the robot entity.
*
* We set this class to be the access controller in Robot's entity annotation.
*
* @see \Drupal\config_entity_example\Entity\Robot
*
* @ingroup config_entity_example
*/
class RobotAccessController extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
// The $opereration parameter tells you what sort of operation access is
// being checked for.
if ($operation == 'view') {
return TRUE;
}
// Other than the view operation, we're going to be insanely lax about
// access. Don't try this at home!
return parent::checkAccess($entity, $operation, $account);
}
}
@@ -1,17 +0,0 @@
{#
Description text for the Config Entity Example.
#}
{% trans %}
<p>The Config Entity Example module defines a Robot entity type. This is a list
of the Robot entities currently in your Drupal site.</p><p>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.</p><p>You can view a list of
Robots here. You can also use the 'Operations' column to edit and delete
Robots.</p>
{% endtrans %}
@@ -1,172 +0,0 @@
<?php
namespace Drupal\Tests\config_entity_example\Functional;
use Drupal\config_entity_example\Entity\Robot;
use Drupal\Tests\BrowserTestBase;
/**
* Test the Config Entity Example module.
*
* @group config_entity_example
* @group examples
*
* @ingroup config_entity_example
*/
class ConfigEntityExampleTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['config_entity_example'];
/**
* The installation profile to use with this test.
*
* We need the 'minimal' profile in order to make sure the Tool block is
* available.
*
* @var string
*/
protected $profile = 'minimal';
/**
* Various functional test of the Config Entity Example module.
*
* 1) Verify that the Marvin entity was created when the module was installed.
*
* 2) Verify that permissions are applied to the various defined paths.
*
* 3) Verify that we can manage entities through the user interface.
*
* 4) Verify that the entity we add can be re-edited.
*
* 5) Verify that the label is shown in the list.
*/
public function testConfigEntityExample() {
$assert = $this->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);
}
}
@@ -1,15 +0,0 @@
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
@@ -1,11 +0,0 @@
# 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
@@ -1,12 +0,0 @@
# 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
@@ -1,22 +0,0 @@
# 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
@@ -1,45 +0,0 @@
<?php
/**
* @file
* Contains Drupal\content_entity_example\content_entity_example.module.
*/
/**
* @defgroup content_entity_example Example: Content Entity
* @ingroup examples
* @{
* Implement a content entity.
*
* This module demonstrates implementing a content entity.
*
* Entity API is the API that stores pieces of content and configuration for
* Drupal core. For instance, if you've encountered node content types, you've
* encountered entities of type 'node.'
*
* This example implements the Entity API so that we have an entity type usable
* by the user which you might think of as specialized nodes, but which are
* different from nodes. These entities are called Contact, and are known
* internally by the machine name content_entity_example_contact.
*
* Contact is a fieldable content entity used to hold structured information
* without the overhead of using a node content type. 'Fieldable' means you can
* attach fields to it, like you can with nodes. It is defined programmatically
* (completely in code). We will show the main techniques to handle and expose
* the contents of this entity type.
*
* The Contact entity will demonstrate the main tasks for an entity:
* - define
* - save
* - load
* - view
* - edit
* - delete
* - control access
*
* Where ever possible, we use the amazing tools built into D8 natively.
*
* @see Drupal\content_entity_example\Entity\Contact
* @see config_entity_example
* }
*/
@@ -1,10 +0,0 @@
'delete contact entity':
title: Delete entity content.
'add contact entity':
title: Add entity content
'view contact entity':
title: View entity content
'edit contact entity':
title: Edit entity content
'administer contact entity':
title: Administer settings
@@ -1,58 +0,0 @@
# This file brings everything together. Very nifty!
# Route name can be used in several places; e.g. links, redirects, and local
# actions.
entity.content_entity_example_contact.canonical:
path: '/content_entity_example_contact/{content_entity_example_contact}'
defaults:
# Calls the view controller, defined in the annotation of the contact entity
_entity_view: 'content_entity_example_contact'
_title: 'Contact content'
requirements:
# Calls the access controller of the entity, $operation 'view'
_entity_access: 'content_entity_example_contact.view'
entity.content_entity_example_contact.collection:
path: '/content_entity_example_contact/list'
defaults:
# Calls the list controller, defined in the annotation of the contact entity.
_entity_list: 'content_entity_example_contact'
_title: 'Contact list'
requirements:
# Checks for permission directly.
_permission: 'view contact entity'
content_entity_example.contact_add:
path: '/content_entity_example_contact/add'
defaults:
# Calls the form.add controller, defined in the contact entity.
_entity_form: content_entity_example_contact.add
_title: 'Add contact'
requirements:
_entity_create_access: 'content_entity_example_contact'
entity.content_entity_example_contact.edit_form:
path: '/content_entity_example_contact/{content_entity_example_contact}/edit'
defaults:
# Calls the form.edit controller, defined in the contact entity.
_entity_form: content_entity_example_contact.edit
_title: 'Edit contact'
requirements:
_entity_access: 'content_entity_example_contact.edit'
entity.content_entity_example_contact.delete_form:
path: '/contact/{content_entity_example_contact}/delete'
defaults:
# Calls the form.delete controller, defined in the contact entity.
_entity_form: content_entity_example_contact.delete
_title: 'Delete contact'
requirements:
_entity_access: 'content_entity_example_contact.delete'
content_entity_example.contact_settings:
path: 'admin/structure/content_entity_example_contact_settings'
defaults:
_form: '\Drupal\content_entity_example\Form\ContactSettingsForm'
_title: 'Contact settings'
requirements:
_permission: 'administer contact entity'
@@ -1,47 +0,0 @@
<?php
namespace Drupal\content_entity_example;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Access controller for the comment entity.
*
* @see \Drupal\comment\Entity\Comment.
*/
class ContactAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*
* Link the activities to the permissions. checkAccess is called with the
* $operation as defined in the routing.yml file.
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'view':
return AccessResult::allowedIfHasPermission($account, 'view contact entity');
case 'edit':
return AccessResult::allowedIfHasPermission($account, 'edit contact entity');
case 'delete':
return AccessResult::allowedIfHasPermission($account, 'delete contact entity');
}
return AccessResult::allowed();
}
/**
* {@inheritdoc}
*
* Separate from the checkAccess because the entity does not yet exist, it
* will be created during the 'add' process.
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermission($account, 'add contact entity');
}
}
@@ -1,18 +0,0 @@
<?php
namespace Drupal\content_entity_example;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\Core\Entity\EntityChangedInterface;
/**
* Provides an interface defining a Contact entity.
*
* We have this interface so we can join the other interfaces it extends.
*
* @ingroup content_entity_example
*/
interface ContactInterface extends ContentEntityInterface, EntityOwnerInterface, EntityChangedInterface {
}
@@ -1,350 +0,0 @@
<?php
namespace Drupal\content_entity_example\Entity;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\content_entity_example\ContactInterface;
use Drupal\user\UserInterface;
use Drupal\Core\Entity\EntityChangedTrait;
/**
* Defines the ContentEntityExample entity.
*
* @ingroup content_entity_example
*
* This is the main definition of the entity type. From it, an entityType is
* derived. The most important properties in this example are listed below.
*
* id: The unique identifier of this entityType. It follows the pattern
* 'moduleName_xyz' to avoid naming conflicts.
*
* label: Human readable name of the entity type.
*
* handlers: Handler classes are used for different tasks. You can use
* standard handlers provided by D8 or build your own, most probably derived
* from the standard class. In detail:
*
* - view_builder: we use the standard controller to view an instance. It is
* called when a route lists an '_entity_view' default for the entityType
* (see routing.yml for details. The view can be manipulated by using the
* standard drupal tools in the settings.
*
* - list_builder: We derive our own list builder class from the
* entityListBuilder to control the presentation.
* If there is a view available for this entity from the views module, it
* overrides the list builder. @todo: any view? naming convention?
*
* - form: We derive our own forms to add functionality like additional fields,
* redirects etc. These forms are called when the routing list an
* '_entity_form' default for the entityType. Depending on the suffix
* (.add/.edit/.delete) in the route, the correct form is called.
*
* - access: Our own accessController where we determine access rights based on
* permissions.
*
* More properties:
*
* - base_table: Define the name of the table used to store the data. Make sure
* it is unique. The schema is automatically determined from the
* BaseFieldDefinitions below. The table is automatically created during
* installation.
*
* - fieldable: Can additional fields be added to the entity via the GUI?
* Analog to content types.
*
* - entity_keys: How to access the fields. Analog to 'nid' or 'uid'.
*
* - links: Provide links to do standard tasks. The 'edit-form' and
* 'delete-form' links are added to the list built by the
* entityListController. They will show up as action buttons in an additional
* column.
*
* There are many more properties to be used in an entity type definition. For
* a complete overview, please refer to the '\Drupal\Core\Entity\EntityType'
* class definition.
*
* The following construct is the actual definition of the entity type which
* is read and cached. Don't forget to clear cache after changes.
*
* @ContentEntityType(
* id = "content_entity_example_contact",
* label = @Translation("Contact entity"),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\content_entity_example\Entity\Controller\ContactListBuilder",
* "form" = {
* "add" = "Drupal\content_entity_example\Form\ContactForm",
* "edit" = "Drupal\content_entity_example\Form\ContactForm",
* "delete" = "Drupal\content_entity_example\Form\ContactDeleteForm",
* },
* "access" = "Drupal\content_entity_example\ContactAccessControlHandler",
* },
* list_cache_contexts = { "user" },
* base_table = "contact",
* admin_permission = "administer content_entity_example entity",
* entity_keys = {
* "id" = "id",
* "label" = "name",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "/content_entity_example_contact/{content_entity_example_contact}",
* "edit-form" = "/content_entity_example_contact/{content_entity_example_contact}/edit",
* "delete-form" = "/contact/{content_entity_example_contact}/delete",
* "collection" = "/content_entity_example_contact/list"
* },
* field_ui_base_route = "content_entity_example.contact_settings",
* )
*
* The 'links' above are defined by their path. For core to find the
* corresponding route, the route name must follow the correct pattern:
*
* entity.<entity-name>.<link-name> (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;
}
}
@@ -1,99 +0,0 @@
<?php
namespace Drupal\content_entity_example\Entity\Controller;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a list controller for content_entity_example entity.
*
* @ingroup content_entity_example
*/
class ContactListBuilder extends EntityListBuilder {
/**
* The url generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->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 <a href="@adminlink">Contacts admin page</a>.', [
'@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);
}
}
@@ -1,56 +0,0 @@
<?php
namespace Drupal\content_entity_example\Form;
use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides a form for deleting a content_entity_example entity.
*
* @ingroup content_entity_example
*/
class ContactDeleteForm extends ContentEntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->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');
}
}
@@ -1,42 +0,0 @@
<?php
namespace Drupal\content_entity_example\Form;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Language\Language;
use Drupal\Core\Form\FormStateInterface;
/**
* Form controller for the content_entity_example entity edit forms.
*
* @ingroup content_entity_example
*/
class ContactForm extends ContentEntityForm {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/* @var $entity \Drupal\content_entity_example\Entity\Contact */
$form = parent::buildForm($form, $form_state);
$entity = $this->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();
}
}
@@ -1,39 +0,0 @@
<?php
namespace Drupal\content_entity_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Class ContentEntityExampleSettingsForm.
*
* @ingroup content_entity_example
*/
class ContactSettingsForm extends FormBase {
/**
* Returns a unique string identifying the form.
*
* @return string
* The unique string identifying the form.
*/
public function getFormId() {
return 'content_entity_example_settings';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Empty implementation of the abstract submit class.
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['contact_settings']['#markup'] = 'Settings form for ContentEntityExample. Manage field settings here.';
return $form;
}
}
@@ -1,249 +0,0 @@
<?php
namespace Drupal\content_entity_example\Tests;
use Drupal\content_entity_example\Entity\Contact;
use Drupal\Tests\examples\Functional\ExamplesBrowserTestBase;
/**
* Tests the basic functions of the Content Entity Example module.
*
* @ingroup content_entity_example
*
* @group content_entity_example
* @group examples
*/
class ContentEntityExampleTest extends ExamplesBrowserTestBase {
public static $modules = ['content_entity_example', 'block', 'field_ui'];
/**
* Basic tests for Content Entity Example.
*/
public function testContentEntityExample() {
$assert = $this->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);
}
}
@@ -1,4 +0,0 @@
# 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
@@ -1,12 +0,0 @@
# 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'
@@ -1,14 +0,0 @@
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
@@ -1,3 +0,0 @@
cron_example.description:
title: Cron Example
route_name: cron_example
@@ -1,56 +0,0 @@
<?php
/**
* @file
* Demonstrates use of the Cron API in Drupal - hook_cron().
*/
/**
* @defgroup cron_example Example: Cron
* @ingroup examples
* @{
* Example using Cron API, including hook_cron() and @QueueWorker plugins
*
* This example is part of the Examples for Developers Project
* which you can download and experiment with at
* http://drupal.org/project/examples
*/
/**
* Implements hook_cron().
*
* We implement hook_cron() to do "background" processing. It gets called every
* time the Drupal cron runs. We then decide what has to happen in response.
*
* In this example, we log a message after the time given in the state value
* 'cron_example.next_execution'. Then we update that variable to a time in the
* future.
*/
function cron_example_cron() {
// We access our configuration.
$cron_config = \Drupal::configFactory()->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".
*/
@@ -1,7 +0,0 @@
cron_example:
path: '/examples/cron-example'
defaults:
_form: '\Drupal\cron_example\Form\CronExampleForm'
_title: 'Cron Example'
requirements:
_permission: 'access content'
@@ -1,256 +0,0 @@
<?php
namespace Drupal\cron_example\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\CronInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form with examples on how to use cron.
*/
class CronExampleForm extends ConfigFormBase {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The cron service.
*
* @var \Drupal\Core\CronInterface
*/
protected $cron;
/**
* The queue object.
*
* @var \Drupal\Core\Queue\QueueFactory
*/
protected $queue;
/**
* The state keyvalue collection.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $current_user, CronInterface $cron, QueueFactory $queue, StateInterface $state) {
parent::__construct($config_factory);
$this->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'];
}
}
@@ -1,92 +0,0 @@
<?php
namespace Drupal\cron_example\Plugin\QueueWorker;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
* Provides base functionality for the ReportWorkers.
*/
abstract class ReportWorkerBase extends QueueWorkerBase implements ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The state.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* ReportWorkerBase constructor.
*
* @param array $configuration
* The configuration of the instance.
* @param string $plugin_id
* The plugin id.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\State\StateInterface $state
* The state service the instance should use.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger
* The logger service the instance should use.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, StateInterface $state, LoggerChannelFactoryInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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),
]);
}
}
@@ -1,29 +0,0 @@
<?php
namespace Drupal\cron_example\Plugin\QueueWorker;
/**
* A report worker.
*
* @QueueWorker(
* id = "cron_example_queue_1",
* title = @Translation("First worker in cron_example"),
* cron = {"time" = 1}
* )
*
* QueueWorkers are new in Drupal 8. They define a queue, which in this case
* is identified as cron_example_queue_1 and contain a process that operates on
* all the data given to the queue.
*
* @see queue_example.module
*/
class ReportWorkerOne extends ReportWorkerBase {
/**
* {@inheritdoc}
*/
public function processItem($data) {
$this->reportWork(1, $data);
}
}
@@ -1,29 +0,0 @@
<?php
namespace Drupal\cron_example\Plugin\QueueWorker;
/**
* A report worker.
*
* @QueueWorker(
* id = "cron_example_queue_2",
* title = @Translation("Second worker in cron_example"),
* cron = {"time" = 20}
* )
*
* QueueWorkers are new in Drupal 8. They define a queue, which in this case
* is identified as cron_example_queue_2 and contain a process that operates on
* all the data given to the queue.
*
* @see queue_example.module
*/
class ReportWorkerTwo extends ReportWorkerBase {
/**
* {@inheritdoc}
*/
public function processItem($data) {
$this->reportWork(2, $data);
}
}
@@ -1,90 +0,0 @@
<?php
namespace Drupal\Tests\cron_example\Functional;
use Drupal\Tests\examples\Functional\ExamplesBrowserTestBase;
/**
* Test the functionality for the Cron Example.
*
* @ingroup cron_example
*
* @group cron_example
* @group examples
*/
class CronExampleTest extends ExamplesBrowserTestBase {
/**
* An editable config object for access to 'cron_example.settings'.
*
* @var \Drupal\Core\Config\Config
*/
protected $cronConfig;
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['cron_example', 'node'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create user. Search content permission granted for the search block to
// be shown.
$this->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 /');
}
}

Some files were not shown because too many files have changed in this diff Show More