From 5ec2f4311ca2ce332f9fa906f7fbc5332dac4a8d Mon Sep 17 00:00:00 2001 From: Bachir Soussi Chiadmi Date: Sat, 6 Jan 2018 11:28:03 +0100 Subject: [PATCH] added example for developpers module --- sites/all/modules/examples/examples/.eslintrc | 95 ++ .../modules/examples/examples/CONTRIBUTING.md | 63 ++ .../all/modules/examples/examples/LICENSE.txt | 339 +++++++ sites/all/modules/examples/examples/README.md | 79 ++ .../modules/examples/examples/STANDARDS.md | 86 ++ .../all/modules/examples/examples/TESTING.md | 114 +++ .../ajax_example/ajax_example.info.yml | 14 + .../ajax_example/ajax_example.libraries.yml | 7 + .../ajax_example/ajax_example.links.menu.yml | 72 ++ .../examples/ajax_example/ajax_example.module | 23 + .../ajax_example/ajax_example.routing.yml | 96 ++ .../ajax_example/css/ajax-example-base.css | 9 + .../examples/ajax_example/js/ajax-example.js | 19 + .../src/Controller/AjaxExampleController.php | 112 +++ .../ajax_example/src/Form/Autotextfields.php | 124 +++ .../src/Form/DependentDropdown.php | 269 ++++++ .../src/Form/DynamicFormSections.php | 222 +++++ .../src/Form/EntityAutocomplete.php | 126 +++ .../ajax_example/src/Form/Simplest.php | 80 ++ .../ajax_example/src/Form/SubmitDriven.php | 73 ++ .../examples/ajax_example/src/Form/Wizard.php | 229 +++++ .../templates/description.html.twig | 29 + .../src/Functional/AjaxExampleMenuTest.php | 79 ++ .../src/Functional/DependentDropdownTest.php | 74 ++ .../Functional/DynamicFormSectionsTest.php | 70 ++ .../AutotextfieldsTest.php | 80 ++ .../DependentDropdownTest.php | 75 ++ .../DynamicFormSectionsTest.php | 67 ++ .../EntityAutocompleteTest.php | 69 ++ .../src/FunctionalJavascript/SimplestTest.php | 60 ++ .../FunctionalJavascript/SubmitDrivenTest.php | 45 + .../batch_example/batch_example.info.yml | 14 + .../batch_example/batch_example.install | 83 ++ .../batch_example.links.menu.yml | 5 + .../batch_example/batch_example.module | 133 +++ .../batch_example/batch_example.routing.yml | 7 + .../src/Form/BatchExampleForm.php | 148 +++ .../src/functional/BatchExampleWebTest.php | 40 + .../block_example/block_example.info.yml | 15 + .../block_example.links.menu.yml | 3 + .../block_example/block_example.module | 53 ++ .../block_example/block_example.routing.yml | 7 + .../config/schema/block_example.schema.yml | 7 + .../src/Controller/BlockExampleController.php | 19 + .../Block/ExampleConfigurableTextBlock.php | 82 ++ .../src/Plugin/Block/ExampleEmptyBlock.php | 30 + .../Plugin/Block/ExampleUppercaseBlock.php | 26 + .../templates/description.html.twig | 15 + .../src/Functional/BlockExampleMenuTest.php | 50 + .../tests/src/Functional/BlockExampleTest.php | 87 ++ .../cache_example/cache_example.info.yml | 14 + .../cache_example.links.menu.yml | 4 + .../cache_example/cache_example.module | 36 + .../cache_example/cache_example.routing.yml | 6 + .../src/Form/CacheExampleForm.php | 306 ++++++ .../tests/src/Functional/CacheExampleTest.php | 110 +++ .../modules/examples/examples/composer.json | 12 + .../config_entity_example.robot.marvin.yml | 20 + .../schema/config_entity_example.schema.yml | 28 + .../config_entity_example.info.yml | 13 + .../config_entity_example.links.action.yml | 14 + .../config_entity_example.links.menu.yml | 3 + .../config_entity_example.module | 42 + .../config_entity_example.permissions.yml | 4 + .../config_entity_example.routing.yml | 56 ++ .../src/Controller/RobotListBuilder.php | 87 ++ .../src/Entity/Robot.php | 89 ++ .../src/Form/RobotAddForm.php | 35 + .../src/Form/RobotDeleteForm.php | 89 ++ .../src/Form/RobotEditForm.php | 35 + .../src/Form/RobotFormBase.php | 221 +++++ .../src/RobotAccessController.php | 34 + .../templates/description.html.twig | 17 + .../Functional/ConfigEntityExampleTest.php | 172 ++++ .../content_entity_example.info.yml | 15 + .../content_entity_example.links.action.yml | 11 + .../content_entity_example.links.menu.yml | 12 + .../content_entity_example.links.task.yml | 22 + .../content_entity_example.module | 45 + .../content_entity_example.permissions.yml | 10 + .../content_entity_example.routing.yml | 58 ++ .../src/ContactAccessControlHandler.php | 47 + .../src/ContactInterface.php | 18 + .../src/Entity/Contact.php | 350 +++++++ .../Entity/Controller/ContactListBuilder.php | 99 ++ .../src/Form/ContactDeleteForm.php | 56 ++ .../src/Form/ContactForm.php | 42 + .../src/Form/ContactSettingsForm.php | 39 + .../Functional/ContentEntityExampleTest.php | 249 +++++ .../config/install/cron_example.settings.yml | 4 + .../config/schema/cron_example.schema.yml | 12 + .../cron_example/cron_example.info.yml | 14 + .../cron_example/cron_example.links.menu.yml | 3 + .../examples/cron_example/cron_example.module | 56 ++ .../cron_example/cron_example.routing.yml | 7 + .../cron_example/src/Form/CronExampleForm.php | 256 +++++ .../Plugin/QueueWorker/ReportWorkerBase.php | 92 ++ .../Plugin/QueueWorker/ReportWorkerOne.php | 29 + .../Plugin/QueueWorker/ReportWorkerTwo.php | 29 + .../tests/src/Functional/CronExampleTest.php | 90 ++ .../examples/examples/css/examples.icons.css | 13 + .../dbtng_example/dbtng_example.info.yml | 15 + .../dbtng_example/dbtng_example.install | 95 ++ .../dbtng_example.links.menu.yml | 25 + .../dbtng_example/dbtng_example.module | 63 ++ .../dbtng_example/dbtng_example.routing.yml | 39 + .../src/Controller/DbtngExampleController.php | 78 ++ .../dbtng_example/src/DbtngExampleStorage.php | 222 +++++ .../src/Form/DbtngExampleAddForm.php | 130 +++ .../src/Form/DbtngExampleUpdateForm.php | 153 +++ .../tests/src/Functional/DbtngExampleTest.php | 222 +++++ .../email_example/email_example.info.yml | 14 + .../email_example.links.menu.yml | 4 + .../email_example/email_example.module | 99 ++ .../email_example/email_example.routing.yml | 6 + .../src/Form/EmailExampleGetFormPage.php | 148 +++ .../tests/src/Functional/EmailExampleTest.php | 73 ++ .../events_example/events_example.info.yml | 13 + .../events_example.links.menu.yml | 4 + .../events_example/events_example.module | 75 ++ .../events_example/events_example.routing.yml | 8 + .../events_example.services.yml | 16 + .../src/Event/IncidentEvents.php | 58 ++ .../src/Event/IncidentReportEvent.php | 69 ++ .../EventsExampleSubscriber.php | 111 +++ .../src/Form/EventsExampleForm.php | 130 +++ .../src/Functional/EventsExampleTest.php | 64 ++ .../src/Kernel/EventsExampleServiceTest.php | 29 + .../examples/examples/examples.info.yml | 13 + .../examples/examples/examples.libraries.yml | 5 + .../modules/examples/examples/examples.module | 131 +++ .../config/schema/field_example.schema.yml | 12 + .../field_example/field_example.info.yml | 15 + .../examples/field_example/field_example.js | 27 + .../field_example/field_example.libraries.yml | 4 + .../field_example.links.menu.yml | 3 + .../field_example/field_example.module | 34 + .../field_example/field_example.routing.yml | 7 + .../src/Controller/FieldExampleController.php | 24 + .../src/FieldExampleWebTestBase.php | 162 ++++ .../ColorBackgroudFormatter.php | 40 + .../FieldFormatter/SimpleTextFormatter.php | 45 + .../src/Plugin/Field/FieldType/RgbItem.php | 55 ++ .../Field/FieldWidget/ColorPickerWidget.php | 43 + .../Plugin/Field/FieldWidget/Text3Widget.php | 87 ++ .../Plugin/Field/FieldWidget/TextWidget.php | 54 ++ .../templates/description.html.twig | 12 + .../ColorBackgroundFormatterTest.php | 98 ++ .../src/Functional/ColorPickerWidgetTest.php | 99 ++ .../FieldExampleBrowserTestBase.php | 167 ++++ .../src/Functional/FieldExampleMenuTest.php | 50 + .../tests/src/Functional/Text3WidgetTest.php | 104 +++ .../tests/src/Functional/TextWidgetTest.php | 104 +++ .../css/field-permission-description.css | 3 + .../css/field_permission_example.css | 20 + .../field_permission_example.info.yml | 14 + .../field_permission_example.libraries.yml | 10 + .../field_permission_example.links.menu.yml | 3 + .../field_permission_example.module | 196 ++++ .../field_permission_example.permissions.yml | 11 + .../field_permission_example.routing.yml | 7 + .../FieldPermissionExampleController.php | 28 + .../FieldFormatter/SimpleTextFormatter.php | 49 + .../src/Plugin/Field/FieldType/FieldNote.php | 55 ++ .../Plugin/Field/FieldWidget/TextWidget.php | 35 + .../templates/description.html.twig | 28 + .../tests/src/Kernel/FieldNoteItemTest.php | 290 ++++++ .../file_example/file_example.info.yml | 15 + .../file_example/file_example.links.menu.yml | 7 + .../examples/file_example/file_example.module | 97 ++ .../file_example/file_example.permissions.yml | 13 + .../file_example/file_example.routing.yml | 8 + .../src/Form/FileExampleReadWriteForm.php | 861 +++++++++++++++++ .../tests/src/Functional/FileExampleTest.php | 152 +++ .../form_api_example.info.yml | 15 + .../form_api_example.links.menu.yml | 78 ++ .../form_api_example/form_api_example.module | 43 + .../form_api_example.routing.yml | 100 ++ .../form_api_example/src/Controller/Page.php | 21 + .../form_api_example/src/Form/AjaxAddMore.php | 144 +++ .../src/Form/AjaxColorForm.php | 147 +++ .../form_api_example/src/Form/BuildDemo.php | 158 ++++ .../src/Form/ContainerDemo.php | 95 ++ .../form_api_example/src/Form/DemoBase.php | 46 + .../form_api_example/src/Form/InputDemo.php | 325 +++++++ .../form_api_example/src/Form/ModalForm.php | 192 ++++ .../src/Form/MultistepForm.php | 193 ++++ .../form_api_example/src/Form/SimpleForm.php | 114 +++ .../form_api_example/src/Form/StateDemo.php | 89 ++ .../src/Form/VerticalTabsDemo.php | 76 ++ .../templates/description.html.twig | 31 + .../tests/src/Functional/FapiExampleTest.php | 325 +++++++ .../src/Functional/MultistepFormTest.php | 68 ++ .../AjaxColorFormTest.php | 83 ++ .../FunctionalJavascript/ModalFormTest.php | 75 ++ .../hooks_example/hooks_example.api.php | 53 ++ .../hooks_example/hooks_example.info.yml | 14 + .../hooks_example/hooks_example.module | 226 +++++ .../hooks_example/hooks_example.routing.yml | 8 + .../src/Controller/HooksExampleController.php | 23 + .../templates/description.html.twig | 30 + .../tests/src/Functional/HooksExampleTest.php | 84 ++ .../examples/examples/images/100x30.svg | 13 + .../examples/examples/images/examples.svg | 56 ++ .../examples/js_example/css/jsweights.css | 4 + .../examples/examples/js_example/js/black.js | 20 + .../examples/examples/js_example/js/blue.js | 20 + .../examples/examples/js_example/js/brown.js | 20 + .../examples/examples/js_example/js/green.js | 20 + .../js_example/js/js_example_accordion.js | 13 + .../examples/examples/js_example/js/purple.js | 20 + .../examples/examples/js_example/js/red.js | 20 + .../examples/js_example/js_example.info.yml | 14 + .../js_example/js_example.libraries.yml | 28 + .../js_example/js_example.links.menu.yml | 18 + .../examples/js_example/js_example.module | 37 + .../js_example/js_example.routing.yml | 23 + .../src/Controller/JsExampleController.php | 111 +++ .../js_example/templates/accordion.html.twig | 60 ++ .../templates/description.html.twig | 23 + .../tests/src/Functional/JsExampleTest.php | 40 + ...isplay.node.basic_content_type.default.yml | 64 ++ ...splay.node.locked_content_type.default.yml | 64 ++ ...isplay.node.basic_content_type.default.yml | 26 + ...splay.node.locked_content_type.default.yml | 26 + ...eld.field.node.basic_content_type.body.yml | 25 + ...ld.field.node.locked_content_type.body.yml | 25 + .../install/node.type.basic_content_type.yml | 14 + .../install/node.type.locked_content_type.yml | 14 + .../node_type_example.info.yml | 16 + .../node_type_example.install | 43 + .../node_type_example.links.menu.yml | 3 + .../node_type_example.module | 44 + .../node_type_example.routing.yml | 6 + .../Controller/NodeTypeExampleController.php | 22 + .../templates/description.html.twig | 39 + .../src/Functional/NodeTypeExampleTest.php | 175 ++++ .../page_example/page_example.info.yml | 14 + .../page_example/page_example.links.menu.yml | 12 + .../examples/page_example/page_example.module | 60 ++ .../page_example/page_example.permissions.yml | 11 + .../page_example/page_example.routing.yml | 55 ++ .../src/Controller/PageExampleController.php | 87 ++ .../templates/description.html.twig | 15 + .../tests/src/Functional/PageExampleTest.php | 176 ++++ .../pager_example/pager_example.info.yml | 14 + .../pager_example.links.menu.yml | 3 + .../pager_example/pager_example.module | 33 + .../pager_example/pager_example.routing.yml | 6 + .../src/Controller/PagerExamplePage.php | 175 ++++ .../templates/description.html.twig | 20 + .../tests/src/Functional/PagerExampleTest.php | 68 ++ .../modules/examples/examples/phpcs.xml.dist | 68 ++ .../phpunit_example/phpunit_example.info.yml | 14 + .../phpunit_example.links.menu.yml | 3 + .../phpunit_example/phpunit_example.module | 17 + .../phpunit_example.routing.yml | 8 + .../examples/phpunit_example/src/AddClass.php | 36 + .../Controller/PHPUnitExampleController.php | 25 + .../src/DisplayInfoInterface.php | 34 + .../phpunit_example/src/DisplayManager.php | 69 ++ .../phpunit_example/src/ProtectedPrivates.php | 58 ++ .../templates/description.html.twig | 55 ++ .../src/Functional/PHPUnitExampleMenuTest.php | 70 ++ .../tests/src/Unit/AddClassTest.php | 180 ++++ .../tests/src/Unit/DisplayManagerTest.php | 59 ++ .../tests/src/Unit/ProtectedPrivatesTest.php | 149 +++ .../Subclasses/ProtectedPrivatesSubclass.php | 30 + .../plugin_type_example.api.php | 24 + .../plugin_type_example.info.yml | 14 + .../plugin_type_example.links.menu.yml | 3 + .../plugin_type_example.module | 71 ++ .../plugin_type_example.routing.yml | 8 + .../plugin_type_example.services.yml | 11 + .../src/Annotation/Sandwich.php | 47 + .../PluginTypeExampleController.php | 125 +++ .../Plugin/Sandwich/ExampleHamSandwich.php | 51 + .../Sandwich/ExampleMeatballSandwich.php | 106 +++ .../plugin_type_example/src/SandwichBase.php | 53 ++ .../src/SandwichInterface.php | 54 ++ .../src/SandwichPluginManager.php | 77 ++ .../src/Functional/PluginTypeExampleTest.php | 82 ++ .../queue_example/queue_example.info.yml | 13 + .../queue_example.links.menu.yml | 3 + .../queue_example/queue_example.module | 37 + .../queue_example/queue_example.routing.yml | 7 + .../src/Forms/QueueExampleForm.php | 441 +++++++++ .../tests/src/Functional/QueueExampleTest.php | 68 ++ ...isplay.node.simpletest_example.default.yml | 64 ++ ...eld.field.node.simpletest_example.body.yml | 25 + .../install/node.type.simpletest_example.yml | 14 + .../simpletest_example_test.info.yml | 14 + .../simpletest_example_test.module | 38 + .../simpletest_example.info.yml | 22 + .../simpletest_example.links.menu.yml | 3 + .../simpletest_example.module | 60 ++ .../simpletest_example.permissions.yml | 6 + .../simpletest_example.routing.yml | 8 + .../SimpleTestExampleController.php | 21 + .../Tests/SimpleTestExampleMockModuleTest.php | 71 ++ .../src/Tests/SimpleTestExampleTest.php | 168 ++++ .../templates/description.html.twig | 18 + .../src/Functional/SimpletestExampeTest.php | 48 + .../examples/src/Tests/ExamplesTest.php | 73 ++ .../examples/src/Tests/ExamplesTestBase.php | 41 + .../src/Utility/DescriptionTemplateTrait.php | 78 ++ .../StreamWrapperExampleController.php | 24 + .../PathProcessor/PathProcessorSessions.php | 30 + .../FileExampleSessionStreamWrapper.php | 879 ++++++++++++++++++ .../src/StreamWrapper/MockSessionTrait.php | 100 ++ .../src/StreamWrapper/SessionWrapper.php | 247 +++++ .../stream_wrapper_example.info.yml | 13 + .../stream_wrapper_example.links.menu.yml | 4 + .../stream_wrapper_example.module | 108 +++ .../stream_wrapper_example.routing.yml | 51 + .../stream_wrapper_example.services.yml | 34 + .../templates/description.html.twig | 44 + .../tests/src/Kernel/StreamWrapperTest.php | 159 ++++ .../tests/src/Unit/SessionWrapperTest.php | 101 ++ .../Controller/TableDragExampleController.php | 24 + .../src/Form/TableDragExampleNestedForm.php | 277 ++++++ .../src/Form/TableDragExampleResetForm.php | 97 ++ .../src/Form/TableDragExampleRootLeafForm.php | 309 ++++++ .../src/Form/TableDragExampleSimpleForm.php | 154 +++ .../tabledrag_example.data.inc | 77 ++ .../tabledrag_example.info.yml | 15 + .../tabledrag_example.install | 101 ++ .../tabledrag_example.links.menu.yml | 28 + .../tabledrag_example.module | 52 ++ .../tabledrag_example.routing.yml | 39 + .../templates/description.html.twig | 25 + .../src/Functional/TabledragMenuTest.php | 87 ++ .../Controller/TableSortExampleController.php | 87 ++ .../tablesort_example.info.yml | 14 + .../tablesort_example.install | 80 ++ .../tablesort_example.links.menu.yml | 3 + .../tablesort_example.module | 20 + .../tablesort_example.routing.yml | 8 + .../src/Functional/TableSortExampleTest.php | 116 +++ .../src/Controller/ContrivedController.php | 93 ++ .../Controller/TestingExampleController.php | 26 + .../src/Tests/NaiveConversionTest.php | 26 + .../templates/description.html.twig | 79 ++ .../testing_example/testing_example.info.yml | 13 + .../testing_example.links.menu.yml | 8 + .../testing_example/testing_example.module | 17 + .../testing_example.routing.yml | 19 + .../src/Functional/ExampleFunctionalTest.php | 163 ++++ .../FrontPageLinkDependenciesTest.php | 69 ++ .../src/Functional/FrontPageLinkTest.php | 43 + .../tests/src/Functional/SkeletonTest.php | 80 ++ .../src/Functional/TestingExampleMenuTest.php | 49 + .../Kernel/ExampleFixtureManagementTest.php | 121 +++ .../Controller/ContrivedControllerTest.php | 118 +++ .../examples_description_test.info.yml | 13 + .../examples_description_test.routing.yml | 6 + .../Controller/SampleExampleController.php | 34 + .../templates/description.html.twig | 25 + .../Functional/ExamplesBrowserTestBase.php | 47 + .../tests/src/Functional/ExamplesTest.php | 74 ++ .../tests/src/Kernel/DescriptionTraitTest.php | 41 + .../tests/src/Unit/YamlValidationTest.php | 49 + .../config/install/tour.tour.tour-example.yml | 103 ++ .../src/Controller/TourExampleController.php | 24 + .../templates/description.html.twig | 42 + .../tests/src/Functional/TourExampleTest.php | 54 ++ .../tests/src/Functional/TourTestBase.php | 82 ++ .../tests/src/Functional/TourTestBasic.php | 76 ++ .../tour_example/tour_example.info.yml | 15 + .../tour_example/tour_example.links.menu.yml | 3 + .../examples/tour_example/tour_example.module | 38 + .../tour_example/tour_example.routing.yml | 9 + .../examples/ms_ajax_form_example/LICENSE.txt | 339 +++++++ .../examples/ms_ajax_form_example/README.txt | 1 + .../ms_ajax_form_example.info.yml | 12 + .../ms_ajax_form_example.routing.yml | 7 + .../src/Button/BaseButton.php | 26 + .../src/Button/ButtonInterface.php | 38 + .../src/Button/StepOneNextButton.php | 32 + .../src/Button/StepThreeFinishButton.php | 40 + .../src/Button/StepThreePreviousButton.php | 33 + .../src/Button/StepTwoNextButton.php | 32 + .../src/Button/StepTwoPreviousButton.php | 33 + .../src/Form/MultiStepExampleForm.php | 206 ++++ .../src/Manager/StepManager.php | 73 ++ .../src/Step/BaseStep.php | 80 ++ .../src/Step/StepFinalize.php | 38 + .../src/Step/StepInterface.php | 58 ++ .../ms_ajax_form_example/src/Step/StepOne.php | 65 ++ .../src/Step/StepThree.php | 70 ++ .../ms_ajax_form_example/src/Step/StepTwo.php | 68 ++ .../src/Step/StepsEnum.php | 55 ++ .../src/Validator/BaseValidator.php | 31 + .../src/Validator/ValidatorInterface.php | 22 + .../src/Validator/ValidatorRegex.php | 34 + .../src/Validator/ValidatorRequired.php | 19 + 396 files changed, 26299 insertions(+) create mode 100644 sites/all/modules/examples/examples/.eslintrc create mode 100644 sites/all/modules/examples/examples/CONTRIBUTING.md create mode 100644 sites/all/modules/examples/examples/LICENSE.txt create mode 100644 sites/all/modules/examples/examples/README.md create mode 100644 sites/all/modules/examples/examples/STANDARDS.md create mode 100644 sites/all/modules/examples/examples/TESTING.md create mode 100644 sites/all/modules/examples/examples/ajax_example/ajax_example.info.yml create mode 100644 sites/all/modules/examples/examples/ajax_example/ajax_example.libraries.yml create mode 100644 sites/all/modules/examples/examples/ajax_example/ajax_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/ajax_example/ajax_example.module create mode 100644 sites/all/modules/examples/examples/ajax_example/ajax_example.routing.yml create mode 100644 sites/all/modules/examples/examples/ajax_example/css/ajax-example-base.css create mode 100644 sites/all/modules/examples/examples/ajax_example/js/ajax-example.js create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Controller/AjaxExampleController.php create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Form/Autotextfields.php create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Form/DependentDropdown.php create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Form/DynamicFormSections.php create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Form/EntityAutocomplete.php create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Form/Simplest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Form/SubmitDriven.php create mode 100644 sites/all/modules/examples/examples/ajax_example/src/Form/Wizard.php create mode 100644 sites/all/modules/examples/examples/ajax_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DependentDropdownTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DynamicFormSectionsTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/AutotextfieldsTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DependentDropdownTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DynamicFormSectionsTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SimplestTest.php create mode 100644 sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php create mode 100644 sites/all/modules/examples/examples/batch_example/batch_example.info.yml create mode 100644 sites/all/modules/examples/examples/batch_example/batch_example.install create mode 100644 sites/all/modules/examples/examples/batch_example/batch_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/batch_example/batch_example.module create mode 100644 sites/all/modules/examples/examples/batch_example/batch_example.routing.yml create mode 100644 sites/all/modules/examples/examples/batch_example/src/Form/BatchExampleForm.php create mode 100644 sites/all/modules/examples/examples/batch_example/tests/src/functional/BatchExampleWebTest.php create mode 100644 sites/all/modules/examples/examples/block_example/block_example.info.yml create mode 100644 sites/all/modules/examples/examples/block_example/block_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/block_example/block_example.module create mode 100644 sites/all/modules/examples/examples/block_example/block_example.routing.yml create mode 100644 sites/all/modules/examples/examples/block_example/config/schema/block_example.schema.yml create mode 100644 sites/all/modules/examples/examples/block_example/src/Controller/BlockExampleController.php create mode 100644 sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleConfigurableTextBlock.php create mode 100644 sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleEmptyBlock.php create mode 100644 sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleUppercaseBlock.php create mode 100644 sites/all/modules/examples/examples/block_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleMenuTest.php create mode 100644 sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleTest.php create mode 100644 sites/all/modules/examples/examples/cache_example/cache_example.info.yml create mode 100644 sites/all/modules/examples/examples/cache_example/cache_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/cache_example/cache_example.module create mode 100644 sites/all/modules/examples/examples/cache_example/cache_example.routing.yml create mode 100644 sites/all/modules/examples/examples/cache_example/src/Form/CacheExampleForm.php create mode 100644 sites/all/modules/examples/examples/cache_example/tests/src/Functional/CacheExampleTest.php create mode 100644 sites/all/modules/examples/examples/composer.json create mode 100644 sites/all/modules/examples/examples/config_entity_example/config/install/config_entity_example.robot.marvin.yml create mode 100644 sites/all/modules/examples/examples/config_entity_example/config/schema/config_entity_example.schema.yml create mode 100644 sites/all/modules/examples/examples/config_entity_example/config_entity_example.info.yml create mode 100644 sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.action.yml create mode 100644 sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/config_entity_example/config_entity_example.module create mode 100644 sites/all/modules/examples/examples/config_entity_example/config_entity_example.permissions.yml create mode 100644 sites/all/modules/examples/examples/config_entity_example/config_entity_example.routing.yml create mode 100644 sites/all/modules/examples/examples/config_entity_example/src/Controller/RobotListBuilder.php create mode 100644 sites/all/modules/examples/examples/config_entity_example/src/Entity/Robot.php create mode 100644 sites/all/modules/examples/examples/config_entity_example/src/Form/RobotAddForm.php create mode 100644 sites/all/modules/examples/examples/config_entity_example/src/Form/RobotDeleteForm.php create mode 100644 sites/all/modules/examples/examples/config_entity_example/src/Form/RobotEditForm.php create mode 100644 sites/all/modules/examples/examples/config_entity_example/src/Form/RobotFormBase.php create mode 100644 sites/all/modules/examples/examples/config_entity_example/src/RobotAccessController.php create mode 100644 sites/all/modules/examples/examples/config_entity_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/config_entity_example/tests/src/Functional/ConfigEntityExampleTest.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/content_entity_example.info.yml create mode 100644 sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.action.yml create mode 100644 sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.task.yml create mode 100644 sites/all/modules/examples/examples/content_entity_example/content_entity_example.module create mode 100644 sites/all/modules/examples/examples/content_entity_example/content_entity_example.permissions.yml create mode 100644 sites/all/modules/examples/examples/content_entity_example/content_entity_example.routing.yml create mode 100644 sites/all/modules/examples/examples/content_entity_example/src/ContactAccessControlHandler.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/src/ContactInterface.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/src/Entity/Contact.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/src/Entity/Controller/ContactListBuilder.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/src/Form/ContactDeleteForm.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/src/Form/ContactForm.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/src/Form/ContactSettingsForm.php create mode 100644 sites/all/modules/examples/examples/content_entity_example/tests/src/Functional/ContentEntityExampleTest.php create mode 100644 sites/all/modules/examples/examples/cron_example/config/install/cron_example.settings.yml create mode 100644 sites/all/modules/examples/examples/cron_example/config/schema/cron_example.schema.yml create mode 100644 sites/all/modules/examples/examples/cron_example/cron_example.info.yml create mode 100644 sites/all/modules/examples/examples/cron_example/cron_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/cron_example/cron_example.module create mode 100644 sites/all/modules/examples/examples/cron_example/cron_example.routing.yml create mode 100644 sites/all/modules/examples/examples/cron_example/src/Form/CronExampleForm.php create mode 100644 sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerBase.php create mode 100644 sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerOne.php create mode 100644 sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerTwo.php create mode 100644 sites/all/modules/examples/examples/cron_example/tests/src/Functional/CronExampleTest.php create mode 100644 sites/all/modules/examples/examples/css/examples.icons.css create mode 100644 sites/all/modules/examples/examples/dbtng_example/dbtng_example.info.yml create mode 100644 sites/all/modules/examples/examples/dbtng_example/dbtng_example.install create mode 100644 sites/all/modules/examples/examples/dbtng_example/dbtng_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/dbtng_example/dbtng_example.module create mode 100644 sites/all/modules/examples/examples/dbtng_example/dbtng_example.routing.yml create mode 100644 sites/all/modules/examples/examples/dbtng_example/src/Controller/DbtngExampleController.php create mode 100644 sites/all/modules/examples/examples/dbtng_example/src/DbtngExampleStorage.php create mode 100644 sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleAddForm.php create mode 100644 sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleUpdateForm.php create mode 100644 sites/all/modules/examples/examples/dbtng_example/tests/src/Functional/DbtngExampleTest.php create mode 100644 sites/all/modules/examples/examples/email_example/email_example.info.yml create mode 100644 sites/all/modules/examples/examples/email_example/email_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/email_example/email_example.module create mode 100644 sites/all/modules/examples/examples/email_example/email_example.routing.yml create mode 100644 sites/all/modules/examples/examples/email_example/src/Form/EmailExampleGetFormPage.php create mode 100644 sites/all/modules/examples/examples/email_example/tests/src/Functional/EmailExampleTest.php create mode 100644 sites/all/modules/examples/examples/events_example/events_example.info.yml create mode 100644 sites/all/modules/examples/examples/events_example/events_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/events_example/events_example.module create mode 100644 sites/all/modules/examples/examples/events_example/events_example.routing.yml create mode 100644 sites/all/modules/examples/examples/events_example/events_example.services.yml create mode 100644 sites/all/modules/examples/examples/events_example/src/Event/IncidentEvents.php create mode 100644 sites/all/modules/examples/examples/events_example/src/Event/IncidentReportEvent.php create mode 100644 sites/all/modules/examples/examples/events_example/src/EventSubscriber/EventsExampleSubscriber.php create mode 100644 sites/all/modules/examples/examples/events_example/src/Form/EventsExampleForm.php create mode 100644 sites/all/modules/examples/examples/events_example/tests/src/Functional/EventsExampleTest.php create mode 100644 sites/all/modules/examples/examples/events_example/tests/src/Kernel/EventsExampleServiceTest.php create mode 100644 sites/all/modules/examples/examples/examples.info.yml create mode 100644 sites/all/modules/examples/examples/examples.libraries.yml create mode 100644 sites/all/modules/examples/examples/examples.module create mode 100644 sites/all/modules/examples/examples/field_example/config/schema/field_example.schema.yml create mode 100644 sites/all/modules/examples/examples/field_example/field_example.info.yml create mode 100644 sites/all/modules/examples/examples/field_example/field_example.js create mode 100644 sites/all/modules/examples/examples/field_example/field_example.libraries.yml create mode 100644 sites/all/modules/examples/examples/field_example/field_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/field_example/field_example.module create mode 100644 sites/all/modules/examples/examples/field_example/field_example.routing.yml create mode 100644 sites/all/modules/examples/examples/field_example/src/Controller/FieldExampleController.php create mode 100644 sites/all/modules/examples/examples/field_example/src/FieldExampleWebTestBase.php create mode 100644 sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroudFormatter.php create mode 100644 sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php create mode 100644 sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldType/RgbItem.php create mode 100644 sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/ColorPickerWidget.php create mode 100644 sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/Text3Widget.php create mode 100644 sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/TextWidget.php create mode 100644 sites/all/modules/examples/examples/field_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorBackgroundFormatterTest.php create mode 100644 sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorPickerWidgetTest.php create mode 100644 sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleBrowserTestBase.php create mode 100644 sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleMenuTest.php create mode 100644 sites/all/modules/examples/examples/field_example/tests/src/Functional/Text3WidgetTest.php create mode 100644 sites/all/modules/examples/examples/field_example/tests/src/Functional/TextWidgetTest.php create mode 100644 sites/all/modules/examples/examples/field_permission_example/css/field-permission-description.css create mode 100644 sites/all/modules/examples/examples/field_permission_example/css/field_permission_example.css create mode 100644 sites/all/modules/examples/examples/field_permission_example/field_permission_example.info.yml create mode 100644 sites/all/modules/examples/examples/field_permission_example/field_permission_example.libraries.yml create mode 100644 sites/all/modules/examples/examples/field_permission_example/field_permission_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/field_permission_example/field_permission_example.module create mode 100644 sites/all/modules/examples/examples/field_permission_example/field_permission_example.permissions.yml create mode 100644 sites/all/modules/examples/examples/field_permission_example/field_permission_example.routing.yml create mode 100644 sites/all/modules/examples/examples/field_permission_example/src/Controller/FieldPermissionExampleController.php create mode 100644 sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php create mode 100644 sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php create mode 100644 sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php create mode 100644 sites/all/modules/examples/examples/field_permission_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/field_permission_example/tests/src/Kernel/FieldNoteItemTest.php create mode 100644 sites/all/modules/examples/examples/file_example/file_example.info.yml create mode 100644 sites/all/modules/examples/examples/file_example/file_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/file_example/file_example.module create mode 100644 sites/all/modules/examples/examples/file_example/file_example.permissions.yml create mode 100644 sites/all/modules/examples/examples/file_example/file_example.routing.yml create mode 100644 sites/all/modules/examples/examples/file_example/src/Form/FileExampleReadWriteForm.php create mode 100644 sites/all/modules/examples/examples/file_example/tests/src/Functional/FileExampleTest.php create mode 100644 sites/all/modules/examples/examples/form_api_example/form_api_example.info.yml create mode 100644 sites/all/modules/examples/examples/form_api_example/form_api_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/form_api_example/form_api_example.module create mode 100644 sites/all/modules/examples/examples/form_api_example/form_api_example.routing.yml create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Controller/Page.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/AjaxAddMore.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/AjaxColorForm.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/BuildDemo.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/ContainerDemo.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/DemoBase.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/InputDemo.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/ModalForm.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/MultistepForm.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/SimpleForm.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/StateDemo.php create mode 100644 sites/all/modules/examples/examples/form_api_example/src/Form/VerticalTabsDemo.php create mode 100644 sites/all/modules/examples/examples/form_api_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/form_api_example/tests/src/Functional/FapiExampleTest.php create mode 100644 sites/all/modules/examples/examples/form_api_example/tests/src/Functional/MultistepFormTest.php create mode 100644 sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/AjaxColorFormTest.php create mode 100644 sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/ModalFormTest.php create mode 100644 sites/all/modules/examples/examples/hooks_example/hooks_example.api.php create mode 100644 sites/all/modules/examples/examples/hooks_example/hooks_example.info.yml create mode 100644 sites/all/modules/examples/examples/hooks_example/hooks_example.module create mode 100644 sites/all/modules/examples/examples/hooks_example/hooks_example.routing.yml create mode 100644 sites/all/modules/examples/examples/hooks_example/src/Controller/HooksExampleController.php create mode 100644 sites/all/modules/examples/examples/hooks_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/hooks_example/tests/src/Functional/HooksExampleTest.php create mode 100644 sites/all/modules/examples/examples/images/100x30.svg create mode 100644 sites/all/modules/examples/examples/images/examples.svg create mode 100755 sites/all/modules/examples/examples/js_example/css/jsweights.css create mode 100755 sites/all/modules/examples/examples/js_example/js/black.js create mode 100755 sites/all/modules/examples/examples/js_example/js/blue.js create mode 100755 sites/all/modules/examples/examples/js_example/js/brown.js create mode 100755 sites/all/modules/examples/examples/js_example/js/green.js create mode 100644 sites/all/modules/examples/examples/js_example/js/js_example_accordion.js create mode 100755 sites/all/modules/examples/examples/js_example/js/purple.js create mode 100755 sites/all/modules/examples/examples/js_example/js/red.js create mode 100644 sites/all/modules/examples/examples/js_example/js_example.info.yml create mode 100644 sites/all/modules/examples/examples/js_example/js_example.libraries.yml create mode 100644 sites/all/modules/examples/examples/js_example/js_example.links.menu.yml create mode 100755 sites/all/modules/examples/examples/js_example/js_example.module create mode 100755 sites/all/modules/examples/examples/js_example/js_example.routing.yml create mode 100755 sites/all/modules/examples/examples/js_example/src/Controller/JsExampleController.php create mode 100755 sites/all/modules/examples/examples/js_example/templates/accordion.html.twig create mode 100644 sites/all/modules/examples/examples/js_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/js_example/tests/src/Functional/JsExampleTest.php create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.basic_content_type.default.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.locked_content_type.default.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.basic_content_type.default.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.locked_content_type.default.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.basic_content_type.body.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.locked_content_type.body.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/node.type.basic_content_type.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/config/install/node.type.locked_content_type.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/node_type_example.info.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/node_type_example.install create mode 100644 sites/all/modules/examples/examples/node_type_example/node_type_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/node_type_example.module create mode 100644 sites/all/modules/examples/examples/node_type_example/node_type_example.routing.yml create mode 100644 sites/all/modules/examples/examples/node_type_example/src/Controller/NodeTypeExampleController.php create mode 100644 sites/all/modules/examples/examples/node_type_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/node_type_example/tests/src/Functional/NodeTypeExampleTest.php create mode 100644 sites/all/modules/examples/examples/page_example/page_example.info.yml create mode 100644 sites/all/modules/examples/examples/page_example/page_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/page_example/page_example.module create mode 100644 sites/all/modules/examples/examples/page_example/page_example.permissions.yml create mode 100644 sites/all/modules/examples/examples/page_example/page_example.routing.yml create mode 100644 sites/all/modules/examples/examples/page_example/src/Controller/PageExampleController.php create mode 100644 sites/all/modules/examples/examples/page_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/page_example/tests/src/Functional/PageExampleTest.php create mode 100644 sites/all/modules/examples/examples/pager_example/pager_example.info.yml create mode 100644 sites/all/modules/examples/examples/pager_example/pager_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/pager_example/pager_example.module create mode 100644 sites/all/modules/examples/examples/pager_example/pager_example.routing.yml create mode 100644 sites/all/modules/examples/examples/pager_example/src/Controller/PagerExamplePage.php create mode 100644 sites/all/modules/examples/examples/pager_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/pager_example/tests/src/Functional/PagerExampleTest.php create mode 100644 sites/all/modules/examples/examples/phpcs.xml.dist create mode 100644 sites/all/modules/examples/examples/phpunit_example/phpunit_example.info.yml create mode 100644 sites/all/modules/examples/examples/phpunit_example/phpunit_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/phpunit_example/phpunit_example.module create mode 100644 sites/all/modules/examples/examples/phpunit_example/phpunit_example.routing.yml create mode 100644 sites/all/modules/examples/examples/phpunit_example/src/AddClass.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/src/Controller/PHPUnitExampleController.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/src/DisplayInfoInterface.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/src/DisplayManager.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/src/ProtectedPrivates.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/AddClassTest.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/DisplayManagerTest.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/ProtectedPrivatesTest.php create mode 100644 sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/Subclasses/ProtectedPrivatesSubclass.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.api.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.info.yml create mode 100644 sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.module create mode 100644 sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.routing.yml create mode 100644 sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.services.yml create mode 100644 sites/all/modules/examples/examples/plugin_type_example/src/Annotation/Sandwich.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/src/Controller/PluginTypeExampleController.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleMeatballSandwich.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/src/SandwichBase.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/src/SandwichInterface.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/src/SandwichPluginManager.php create mode 100644 sites/all/modules/examples/examples/plugin_type_example/tests/src/Functional/PluginTypeExampleTest.php create mode 100644 sites/all/modules/examples/examples/queue_example/queue_example.info.yml create mode 100644 sites/all/modules/examples/examples/queue_example/queue_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/queue_example/queue_example.module create mode 100644 sites/all/modules/examples/examples/queue_example/queue_example.routing.yml create mode 100644 sites/all/modules/examples/examples/queue_example/src/Forms/QueueExampleForm.php create mode 100644 sites/all/modules/examples/examples/queue_example/tests/src/Functional/QueueExampleTest.php create mode 100644 sites/all/modules/examples/examples/simpletest_example/config/install/core.entity_form_display.node.simpletest_example.default.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/config/install/field.field.node.simpletest_example.body.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/config/install/node.type.simpletest_example.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.info.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.module create mode 100644 sites/all/modules/examples/examples/simpletest_example/simpletest_example.info.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/simpletest_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/simpletest_example.module create mode 100644 sites/all/modules/examples/examples/simpletest_example/simpletest_example.permissions.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/simpletest_example.routing.yml create mode 100644 sites/all/modules/examples/examples/simpletest_example/src/Controller/SimpleTestExampleController.php create mode 100644 sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleMockModuleTest.php create mode 100644 sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleTest.php create mode 100644 sites/all/modules/examples/examples/simpletest_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/simpletest_example/tests/src/Functional/SimpletestExampeTest.php create mode 100644 sites/all/modules/examples/examples/src/Tests/ExamplesTest.php create mode 100644 sites/all/modules/examples/examples/src/Tests/ExamplesTestBase.php create mode 100644 sites/all/modules/examples/examples/src/Utility/DescriptionTemplateTrait.php create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/src/Controller/StreamWrapperExampleController.php create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.info.yml create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.module create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.routing.yml create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.services.yml create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php create mode 100644 sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php create mode 100644 sites/all/modules/examples/examples/tabledrag_example/src/Controller/TableDragExampleController.php create mode 100644 sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleNestedForm.php create mode 100644 sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleResetForm.php create mode 100644 sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleRootLeafForm.php create mode 100644 sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleSimpleForm.php create mode 100644 sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.data.inc create mode 100644 sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.info.yml create mode 100644 sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.install create mode 100644 sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.module create mode 100644 sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.routing.yml create mode 100644 sites/all/modules/examples/examples/tabledrag_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/tabledrag_example/tests/src/Functional/TabledragMenuTest.php create mode 100644 sites/all/modules/examples/examples/tablesort_example/src/Controller/TableSortExampleController.php create mode 100644 sites/all/modules/examples/examples/tablesort_example/tablesort_example.info.yml create mode 100644 sites/all/modules/examples/examples/tablesort_example/tablesort_example.install create mode 100644 sites/all/modules/examples/examples/tablesort_example/tablesort_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/tablesort_example/tablesort_example.module create mode 100644 sites/all/modules/examples/examples/tablesort_example/tablesort_example.routing.yml create mode 100644 sites/all/modules/examples/examples/tablesort_example/tests/src/Functional/TableSortExampleTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/src/Controller/ContrivedController.php create mode 100644 sites/all/modules/examples/examples/testing_example/src/Controller/TestingExampleController.php create mode 100644 sites/all/modules/examples/examples/testing_example/src/Tests/NaiveConversionTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/testing_example/testing_example.info.yml create mode 100644 sites/all/modules/examples/examples/testing_example/testing_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/testing_example/testing_example.module create mode 100644 sites/all/modules/examples/examples/testing_example/testing_example.routing.yml create mode 100644 sites/all/modules/examples/examples/testing_example/tests/src/Functional/ExampleFunctionalTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkDependenciesTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/tests/src/Functional/SkeletonTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/tests/src/Functional/TestingExampleMenuTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/tests/src/Kernel/ExampleFixtureManagementTest.php create mode 100644 sites/all/modules/examples/examples/testing_example/tests/src/Unit/Controller/ContrivedControllerTest.php create mode 100644 sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.info.yml create mode 100644 sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.routing.yml create mode 100644 sites/all/modules/examples/examples/tests/modules/examples_description_test/src/Controller/SampleExampleController.php create mode 100644 sites/all/modules/examples/examples/tests/modules/examples_description_test/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/tests/src/Functional/ExamplesBrowserTestBase.php create mode 100644 sites/all/modules/examples/examples/tests/src/Functional/ExamplesTest.php create mode 100644 sites/all/modules/examples/examples/tests/src/Kernel/DescriptionTraitTest.php create mode 100644 sites/all/modules/examples/examples/tests/src/Unit/YamlValidationTest.php create mode 100644 sites/all/modules/examples/examples/tour_example/config/install/tour.tour.tour-example.yml create mode 100644 sites/all/modules/examples/examples/tour_example/src/Controller/TourExampleController.php create mode 100644 sites/all/modules/examples/examples/tour_example/templates/description.html.twig create mode 100644 sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourExampleTest.php create mode 100644 sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBase.php create mode 100644 sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBasic.php create mode 100644 sites/all/modules/examples/examples/tour_example/tour_example.info.yml create mode 100644 sites/all/modules/examples/examples/tour_example/tour_example.links.menu.yml create mode 100644 sites/all/modules/examples/examples/tour_example/tour_example.module create mode 100644 sites/all/modules/examples/examples/tour_example/tour_example.routing.yml create mode 100644 sites/all/modules/examples/ms_ajax_form_example/LICENSE.txt create mode 100644 sites/all/modules/examples/ms_ajax_form_example/README.txt create mode 100644 sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.info.yml create mode 100644 sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.routing.yml create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Button/BaseButton.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Button/ButtonInterface.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Button/StepOneNextButton.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreeFinishButton.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreePreviousButton.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoNextButton.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoPreviousButton.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Form/MultiStepExampleForm.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Manager/StepManager.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Step/BaseStep.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Step/StepFinalize.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Step/StepInterface.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Step/StepOne.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Step/StepThree.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Step/StepTwo.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Step/StepsEnum.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Validator/BaseValidator.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorInterface.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRegex.php create mode 100644 sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRequired.php diff --git a/sites/all/modules/examples/examples/.eslintrc b/sites/all/modules/examples/examples/.eslintrc new file mode 100644 index 000000000..7e88b38d0 --- /dev/null +++ b/sites/all/modules/examples/examples/.eslintrc @@ -0,0 +1,95 @@ +{ + "extends": "eslint:recommended", + "env": { + "browser": true + }, + "globals": { + "Drupal": true, + "drupalSettings": true, + "drupalTranslations": true, + "domready": true, + "jQuery": true, + "_": true, + "matchMedia": true, + "Backbone": true, + "Modernizr": true, + "CKEDITOR": true + }, + "rules": { + // Errors. + "array-bracket-spacing": [2, "never"], + "block-scoped-var": 2, + "brace-style": [2, "stroustrup", {"allowSingleLine": true}], + "comma-dangle": [2, "never"], + "comma-spacing": 2, + "comma-style": [2, "last"], + "computed-property-spacing": [2, "never"], + "curly": [2, "all"], + "eol-last": 2, + "eqeqeq": [2, "smart"], + "guard-for-in": 2, + "indent": [2, 2, {"SwitchCase": 1}], + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "keyword-spacing": [2, {"before": true, "after": true}], + "linebreak-style": [2, "unix"], + "lines-around-comment": [2, {"beforeBlockComment": true, "afterBlockComment": false}], + "new-parens": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-parens": [2, "functions"], + "no-implied-eval": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-nested-ternary": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-wrappers": 2, + "no-octal-escape": 2, + "no-process-exit": 2, + "no-proto": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-undef-init": 2, + "no-undefined": 2, + "no-unused-expressions": 2, + "no-unused-vars": [2, {"vars": "all", "args": "none"}], + "no-with": 2, + "object-curly-spacing": [2, "never"], + "one-var": [2, "never"], + "quote-props": [2, "consistent-as-needed"], + "quotes": [2, "single", "avoid-escape"], + "semi": [2, "always"], + "semi-spacing": [2, {"before": false, "after": true}], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always"], + "strict": 2, + "yoda": [2, "never"], + // Warnings. + "max-nested-callbacks": [1, 3], + "valid-jsdoc": [1, { + "prefer": { + "returns": "return", + "property": "prop" + }, + "requireReturn": false + }] + } +} diff --git a/sites/all/modules/examples/examples/CONTRIBUTING.md b/sites/all/modules/examples/examples/CONTRIBUTING.md new file mode 100644 index 000000000..a39f3a4dc --- /dev/null +++ b/sites/all/modules/examples/examples/CONTRIBUTING.md @@ -0,0 +1,63 @@ +Drupal Examples For Developers: Contributor's Guide +=================================================== + +Examples for Developers is a community project. + +If you'd like to participate in Examples development, thank you! + +If you are new to Drupal or open source in general, have no fear. Examples is +an easy-going project where you can learn some things about how to work on a +collaborative project in a friendly environment. + + +Policies +-------- + +Examples follows the Drupal core process as much as possible. + +Contributions thus need to be similar in quality to Drupal core patches. +Contributions will need to meet the following minimum standards: + +### Normal Drupal issue process + +Drupal projects use patches related to issues. You should know how to make a +patch and an interdiff using git. It's fine to develop on github or +what-have-you, but eventually it has to be a patch that can be reviewed in the +normal Drupal issue process. See the list of resources for some information on +how to do do this. + +Your patch will also need to be reviewed by someone other than yourself. Learn +about the review process in the resources section. + +### DrupalCI + +Examples uses the Drupal automated testing system to verify the applicability of +patches. See `TESTING.md` for details. + +### Drupal coding standards + +All code in Examples should adhere to the Drupal core coding standards. Examples +uses the Drupal Coder project and PHP_CodeSniffer to enforce coding standards. +Think of this as another test your code must pass. See `STANDARDS.md` for +details. + + +Resources +--------- + +### Novice + +Drupal novice contribution guide: https://www.drupal.org/novice + +Drupal contribution guide: https://www.drupal.org/contribute + +What's a patch? https://www.drupal.org/patch + +How to make a patch with git: https://www.drupal.org/node/707484 + +### Everyone + +How to review a patch: https://www.drupal.org/patch/review + +See `STANDARDS.md` and `TESTING.md` for information on how to run a coding +standards test, and also how to run the tests themselves. diff --git a/sites/all/modules/examples/examples/LICENSE.txt b/sites/all/modules/examples/examples/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/sites/all/modules/examples/examples/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/examples/examples/README.md b/sites/all/modules/examples/examples/README.md new file mode 100644 index 000000000..1e4c25b38 --- /dev/null +++ b/sites/all/modules/examples/examples/README.md @@ -0,0 +1,79 @@ +Examples for Developers +======================= + +Project site: http://drupal.org/project/examples + +Code: https://drupal.org/project/examples/git-instructions + +Issues: https://drupal.org/project/issues/examples + +What Is This? +------------- + +This set of modules is intended to provide working examples of Drupal 8's +features and APIs. The modules strive to be simple, well documented and +modification friendly, in order to help developers quickly learn their inner +workings. + +These examples are meant to teach you about code-level development for Drupal +8. Some solutions might be better served using a contributed module, so that +you don't end up having to re-invent the wheel in PHP. When in doubt, look for +an existing contrib project that already does what you want, and contribute to +that project. + + +How To Use The Examples +----------------------- + +There are three main ways to interact with the examples in this project: + +1. Enable the modules and use them within Drupal. Not all modules will have +obvious things to see within your Drupal installation. For instance, while the +Page and Form API examples will display forms, the Database API example does not +have much that is visible within Drupal. + +2. Read the code. Much effort has gone into making the example code readable, +not only in terms of the code itself, but also the extensive inline comments +and documentation blocks. + +3. Browse the code and documentation on the web. There are two main places to +do this: + +* https://api.drupal.org/api/examples is the main API site for all of Drupal. +It has all manner of cross-linked references between the example code and the +APIs being demonstrated. All of the Doxygen-based comments in the code are +parsed and made browseable here. + +* http://drupalcode.org/project/examples.git allows you to browse the git +repository for the Examples project. + +This project ships with a composer.json file. This is meant to illustrate how +to provide a composer.json file for a Drupal contrib project. You can read more +about how to use Composer with Drupal here: https://www.drupal.org/node/2718229 + +How To Install The Modules +-------------------------- + +1. The Examples project installs like any other Drupal module. There is extensive +documentation on how to do this here: +https://drupal.org/documentation/install/modules-themes/modules-8 But essentially: +Download the tarball and expand it into the modules/ directory in your Drupal 8 +installation. + +2. Within Drupal, enable any Example sub-module you wish to explore in Admin +menu > Extend. + +3. Rebuild access permissions if you are prompted to. + +4. Profit! The links for Examples material will appear in your Tools menu. This +menu appears on the left sidebar by default. You'll need to reenable it if you +removed it. + +Having seen the behavior of the various example modules, you can move on to +reading the code, experimenting with it, and hopefully grasp how things work. + +If you find a problem, incorrect comment, obsolete or improper code or such, +please search for an issue about it at http://drupal.org/project/issues/examples +If there isn't already an issue for it, please create a new one. + +Thanks. diff --git a/sites/all/modules/examples/examples/STANDARDS.md b/sites/all/modules/examples/examples/STANDARDS.md new file mode 100644 index 000000000..387656a33 --- /dev/null +++ b/sites/all/modules/examples/examples/STANDARDS.md @@ -0,0 +1,86 @@ +Developing with Coding Standards for Examples for Developers +============================================================ + +Examples uses mostly the same coding standards as Drupal core. + +If you see a discrepancy between the coding standards tools used by core and +those used by Examples, please file an issue so that Examples can follow core. + +Examples uses the `phpcs` tool to allow for checking PHP coding standards. We +use the `drupal/coder` project for Drupal-specific coding standards. + +We also use `eslint` for JavaScript coding standards, and `csslint` for CSS. + +Examples has a `phpcs.xml.dist` file at the root of the project. phpcs uses this +file to specify the current coding standards 'sniffs' which code in the project +must pass. + +Contributors should install `phpcs` in their local Drupal installation, and then +use that to run `phpcs` against Examples as part of their development and review +process. (See details below on how to install and run this tool.) + +Contributors can also patch the `phpcs.xml.dist` file itself, in order to fix +the codebase to pass a given rule or sniff. Patches which do this should be +limited to a single rule or sniff, in order make the patch easier to review. + +Examples also uses the Coder project (`drupal/coder`), which adds additional +Drupal-specific coding standards. We're currently locked to Coder version +8.2.8, but this should change to reflect the state of core's coding standards. + +Installing phpcs +---------------- + +Current versions of Drupal 8 core require phpcs and Coder as dev dependencies. +That means they're already probably installed in your core vendor/ directory. + +We need to tell `phpcs` to use the Drupal coding standard provided by Coder, +because it isn't configured that way by default. + +Like this: + + $ cd my/drupal/root/ + $ ./vendor/bin/phpcs --config-set installed_paths /full/path/to/drupal/vendor/drupal/coder/coder_sniffer/ + // phpcs now knows how to find the Drupal standard. You can test it: + $ cd core + $ ../vendor/bin/phpcs -e --standard=Drupal + // Shows you a bunch of Drupal-related sniffs. + +Running phpcs +------------- + +Now you can run phpcs: + + $ cd modules/examples + $ ../../vendor/bin/phpcs -ps + // phpcs uses Examples' phpcs.xml.dist to verify coding standards. + // -p shows you progress dots. + // -s shows you sniff errors in the report. + +If there are errors, they can sometimes be fixed with `phpcbf`, which is +part of the `phpcs` package. + + $ ../../vendor/bin/phpcbf + // phpcbf now performs automated fixes. + +Always look at the changes to see what `phpcbf` did. + +And always re-run `phpcs` in order to discover whether `phpcbf` handled all the +errors. + +Installing eslint +----------------- + +`eslint` is a node.js tool. You can and probably should install it globally, +since installing it locally would add files to the examples project. +Instructions available here: https://www.npmjs.com/package/eslint + +Examples has an `.eslintrc` file which defines the JavaScript coding standard. +This file should be identical to the current Drupal core standard. + +Running eslint +-------------- + +You can run eslint this way: + + $ cd /path/to/examples + $ eslint . diff --git a/sites/all/modules/examples/examples/TESTING.md b/sites/all/modules/examples/examples/TESTING.md new file mode 100644 index 000000000..d8398832a --- /dev/null +++ b/sites/all/modules/examples/examples/TESTING.md @@ -0,0 +1,114 @@ +Testing Drupal Examples for Developers +====================================== + +The Drupal Examples for Developers project uses DrupalCI testing on drupal.org. + +That means: It runs the testbot on every patch that is marked as 'Needs Review.' + +Your patch might not get reviewed, and certainly won't get committed unless it +passes the testbot. + +The testbot runs a script that's in your Drupal installation called +`core/scripts/run-tests.sh`. You can run `run-tests.sh` manually and approximate +the testbot's behavior. + +You can find information on how to run `run-tests.sh` locally here: +https://www.drupal.org/node/645286 + +Examples is always targeted to the dev branch of Drupal core for the latest +release. As of this writing, the latest release of Drupal core is 8.2.5, which +means development for Examples should be against the Drupal 8.2.x development +branch. When Drupal 8.3.0 is released, we'll start targeting Examples to 8.3.x, +and so on. + +You should at least run `run-tests.sh` locally against all the changes in your +patch before uploading it. + +Keep in mind that unless you know you're changing behavior that is being tested +for, the tests are not at fault. :-) + +Note also that, currently, using the `phpunit` tool under Drupal 8 will not find +PHPUnit-based tests in submodules, such as phpunit_example. There is no +suggested workaround for this, since there is no best practice to demonstrate as +an example. There is, however, this issue in core: +https://www.drupal.org/node/2499239 + +How To Run The Tests In The Drupal UI +------------------------------------- + +Generally, you should run tests from the command line. This is generally easier +than using Drupal's testing UI. However, here's how you can do it that way: + +Enable the Testing module. + +Visit the test list page at `admin/config/development/testing`. + +Since the tests are organized by module, you can search for a module name and +get all the tests for that module. For instance, type in 'node_type_example' for +all the tests related to that module. + +Click the check boxes next to the tests you want to run. If you find this +tedious, it's time to learn to use the command line. :-) + +Click 'Run Tests.' You're now running the tests. + +Step-by-step: How To Run The Tests. +----------------------------------- + +Begin with an installed Drupal codebase. Make a codebase, set up the database, +etc. Note that you can use an existing Drupal instance but the best practice is +to start fresh. Something not working right? Try a new installation. + +Use the dev branch of core for the latest release of Drupal. As of this writing, +it's 8.2.x. When Drupal 8.3.0 is released, we'll target 8.3.x. + +Open the terminal window and move to the root directory of the Drupal +installation: + + $ cd path/to/drupal + +Put Examples into the `modules/` folder of the Drupal installation. If you are +doing development on Examples, you should have already checked out the git +repository into `modules/`, like this: + + $ git clone --branch 8.x-1.x https://git.drupal.org/project/examples.git modules/examples + +Now you can run `run-tests.sh`, which, despite having a `.sh` suffix is not a +shell script. It's a PHP script. + +You'll use the `--directory` option to have the test runner scan the Examples +module directory for tests. + +Also, importantly, if your test site has its own URL, you'll need to supply that +with the `--url` option. For instance, under MAMP, you must specify +`--url http://localhost:8888/`. + +You can also use `--concurrency` to speed up the test run, and `--browser` to +see detailed test results in a web browser instead of just text output in the +terminal. + + $ php ./core/scripts/run-tests.sh --browser --concurrency 10 --url http://localhost:8888/ --directory modules/examples + +This should run all the tests present in Examples. If you add a test and it +doesn't appear in the list of tests to run, then you'll need to double-check +that it's in the proper test namespace and that the class name (and thus the +file name) ends in Test. + +What Tests Should An Example Module Have? +------------------------------------------ + +Examples has a checklist for each module: +https://www.drupal.org/node/2209627 + +The reason we care about these tests is that we want the documentation +of these APIs to be correct. If Core changes APIs, we want our tests to +fail so that we know our documentation is incorrect. + +Our list of required tests includes: +* Functional tests which verifies a 200 result for each route/path defined by + the module. +* Functional tests of permission-based restrictions. +* Functional tests which submit forms and verify that they behave as + expected. +* Unit tests of unit-testable code. +* Other. More. Better. diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.info.yml b/sites/all/modules/examples/examples/ajax_example/ajax_example.info.yml new file mode 100644 index 000000000..57a4145e1 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.info.yml @@ -0,0 +1,14 @@ +name: 'AJAX Example' +type: module +description: 'An example module showing how to use Drupal AJAX forms.' +package: 'Example modules' +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.libraries.yml b/sites/all/modules/examples/examples/ajax_example/ajax_example.libraries.yml new file mode 100644 index 000000000..4df89d8c1 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.libraries.yml @@ -0,0 +1,7 @@ +ajax_example.library: + version: 1.x + css: + base: + css/ajax-example-base.css: {} + js: + js/ajax-example.js: {} diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.links.menu.yml b/sites/all/modules/examples/examples/ajax_example/ajax_example.links.menu.yml new file mode 100644 index 000000000..b0c31d4ba --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.links.menu.yml @@ -0,0 +1,72 @@ +ajax_example.description: + title: 'AJAX Example' + route_name: 'ajax_example.description' + expanded: TRUE + +ajax_example.simplest: + title: 'Simplest AJAX example' + route_name: 'ajax_example.simplest' + parent: ajax_example.description + weight: 0 + +ajax_example.submit-driven: + title: 'Submit-driven AJAX' + route_name: 'ajax_example.submit_driven_ajax' + parent: ajax_example.description + weight: 1 + +ajax_example.render-link: + title: 'AJAX link in a render array' + route_name: 'ajax_example.ajax_link_render' + parent: ajax_example.description + weight: 2 + +ajax_example.wizard-example: + title: 'Wizard example' + route_name: 'ajax_example.wizard' + parent: ajax_example.description + weight: 2 + +ajax_example.wizard-examplenojs: + title: 'Wizard example w/JS turned off' + route_name: 'ajax_example.wizardnojs' + parent: ajax_example.description + weight: 3 + +ajax_example.autocomplete-user: + title: 'Autocomplete user with entity_autocomplete' + route_name: 'ajax_example.autocomplete_user' + parent: ajax_example.description + weight: 4 + +ajax_example.autotextfields: + title: 'Generate textfields' + route_name: 'ajax_example.autotextfields' + parent: ajax_example.description + weight: 5 + +ajax_example.dependent-dropdown: + title: 'Dependent dropdown' + route_name: 'ajax_example.dependent_dropdown' + parent: ajax_example.description + weight: 6 +ajax_example.dependent-dropdown-nojs: + title: 'Dependent dropdown w/ no JS' + route_name: 'ajax_example.dependent_dropdown' + route_parameters: + nojs: nojs + parent: ajax_example.description + weight: 6 + +ajax_example.dynamic-form-sections: + title: 'Dynamic form sections' + route_name: 'ajax_example.dynamic_form_sections' + parent: ajax_example.description + weight: 10 +ajax_example.dynamic-form-sections-nojs: + title: 'Dynamic form sections w/ no JS' + route_name: 'ajax_example.dynamic_form_sections' + route_parameters: + nojs: nojs + parent: ajax_example.description + weight: 10 diff --git a/sites/all/modules/examples/examples/ajax_example/ajax_example.module b/sites/all/modules/examples/examples/ajax_example/ajax_example.module new file mode 100644 index 000000000..1c2a6de0f --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/ajax_example.module @@ -0,0 +1,23 @@ + $this->t(' +The link below has been rendered as an element with the #ajax property, so if +javascript is enabled, ajax.js will try to submit it via an AJAX call instead +of a normal page load. The URL also contains the "/nojs/" magic string, which +is stripped if javascript is enabled, allowing the server code to tell by the +URL whether JS was enabled or not, letting it do different things based on that.'), + ]; + // We'll add a nice border element for our demo. + $build['ajax_link'] = [ + '#type' => 'details', + '#title' => $this->t('This is the AJAX link'), + '#open' => TRUE, + ]; + // We build the AJAX link. + $build['ajax_link']['link'] = [ + '#type' => 'link', + '#title' => $this->t('Click me'), + // We have to ensure that Drupal's Ajax system is loaded. + '#attached' => ['library' => ['core/drupal.ajax']], + // We add the 'use-ajax' class so that Drupal's AJAX system can spring + // into action. + '#attributes' => ['class' => ['use-ajax']], + // The URL for this link element is the callback. In our case, it's route + // ajax_example.ajax_link_callback, which maps to ajaxLinkCallback() + // below. The route has a /{nojs} section, which is how the callback can + // know whether the request was made by AJAX or some other means where + // JavaScript won't be able to handle the result. If the {nojs} part of + // the path is replaced with 'ajax', then the request was made by AJAX. + '#url' => Url::fromRoute('ajax_example.ajax_link_callback', ['nojs' => 'ajax']), + ]; + // We provide a DIV that AJAX can append some text into. + $build['ajax_link']['destination'] = [ + '#type' => 'container', + '#attributes' => ['id' => ['ajax-example-destination-div']], + ]; + return $build; + } + + /** + * Callback for link example. + * + * Takes different logic paths based on whether Javascript was enabled. + * If $type == 'ajax', it tells this function that ajax.js has rewritten + * the URL and thus we are doing an AJAX and can return an array of commands. + * + * @param string $nojs + * Either 'ajax' or 'nojs. Type is simply the normal URL argument to this + * URL. + * + * @return string|array + * If $type == 'ajax', returns an array of AJAX Commands. + * Otherwise, just returns the content, which will end up being a page. + */ + public function ajaxLinkCallback($nojs = 'ajax') { + // Determine whether the request is coming from AJAX or not. + if ($nojs == 'ajax') { + $output = $this->t("This is some content delivered via AJAX"); + $response = new AjaxResponse(); + $response->addCommand(new AppendCommand('#ajax-example-destination-div', $output)); + + // See ajax_example_advanced.inc for more details on the available + // commands and how to use them. + // $page = array('#type' => 'ajax', '#commands' => $commands); + // ajax_deliver($response); + return $response; + } + $response = new Response($this->t("This is some content delivered via a page load.")); + return $response; + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/Autotextfields.php b/sites/all/modules/examples/examples/ajax_example/src/Form/Autotextfields.php new file mode 100644 index 000000000..a1fa173e3 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/src/Form/Autotextfields.php @@ -0,0 +1,124 @@ + 'item', + '#markup' => $this->t('This form demonstrates changing the status of form elements through AJAX requests.'), + ]; + $form['ask_first_name'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Ask me my first name'), + '#ajax' => [ + 'callback' => '::textfieldsCallback', + 'wrapper' => 'textfields-container', + 'effect' => 'fade', + ], + ]; + $form['ask_last_name'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Ask me my last name'), + '#ajax' => [ + 'callback' => '::textfieldsCallback', + 'wrapper' => 'textfields-container', + 'effect' => 'fade', + ], + ]; + + // Wrap textfields in a container. This container will be replaced through + // AJAX. + $form['textfields_container'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'textfields-container'], + ]; + $form['textfields_container']['textfields'] = [ + '#type' => 'fieldset', + '#title' => $this->t("Generated text fields for first and last name"), + '#description' => t('This is where we put automatically generated textfields'), + ]; + + // This form is rebuilt on all requests, so whether or not the request comes + // from AJAX, we should rebuild everything based on the form state. + // Checkbox values are expressed as 1 or 0, so we have to be sure to compare + // type as well as value. + if ($form_state->getValue('ask_first_name', NULL) === 1) { + $form['textfields_container']['textfields']['first_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('First Name'), + '#required' => TRUE, + ]; + } + if ($form_state->getValue('ask_last_name', NULL) === 1) { + $form['textfields_container']['textfields']['last_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Last Name'), + '#required' => TRUE, + ]; + } + + $form['textfields_container']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Click Me'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + drupal_set_message( + $this->t('Submit handler: First name: @first_name Last name: @last_name', + [ + '@first_name' => $form_state->getValue('first_name', 'n/a'), + '@last_name' => $form_state->getValue('last_name', 'n/a'), + ] + ) + ); + } + + /** + * Callback for ajax_example_autotextfields. + * + * Selects the piece of the form we want to use as replacement markup and + * returns it as a form (renderable array). + */ + public function textfieldsCallback($form, FormStateInterface $form_state) { + return $form['textfields_container']; + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/DependentDropdown.php b/sites/all/modules/examples/examples/ajax_example/src/Form/DependentDropdown.php new file mode 100644 index 000000000..5c2e41ada --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/src/Form/DependentDropdown.php @@ -0,0 +1,269 @@ + + $this->t('

Like other examples in this module, this form has a path that + can be modified with /nojs to simulate its behavior without JavaScript. +

    +
  • @try_it_without_ajax
  • +
  • @try_it_with_ajax
  • +
', + [ + '@try_it_without_ajax' => Link::createFromRoute( + $this->t('Try it without AJAX'), + 'ajax_example.dependent_dropdown', ['nojs' => 'nojs']) + ->toString(), + '@try_it_with_ajax' => Link::createFromRoute( + $this->t('Try it with AJAX'), + 'ajax_example.dependent_dropdown') + ->toString(), + ] + ), + ]; + + // Our first dropdown lets us select a family of instruments: String, + // Woodwind, Brass, or Percussion. + $instrument_family_options = static::getFirstDropdownOptions(); + // When the AJAX request occurs, this form will be build in order to process + // form state before the AJAX callback is called. We can use this + // opportunity to populate the form as we wish based on the changes to the + // form that caused the AJAX request. If the user caused the AJAX request, + // then it would have been setting a value for instrument_family_options. + // So if there's a value in that dropdown before we build it here, we grab + // it's value to help us build the specific instrument dropdown. Otherwise + // we can just use the value of the first item as the default value. + if (empty($form_state->getValue('instrument_family_dropdown'))) { + // Use a default value. + $selected_family = key($instrument_family_options); + } + else { + // Get the value if it already exists. + $selected_family = $form_state->getValue('instrument_family_dropdown'); + } + + $form['instrument_family_fieldset'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Choose an instrument family'), + ]; + $form['instrument_family_fieldset']['instrument_family_dropdown'] = [ + '#type' => 'select', + '#title' => $this->t('Instrument Type'), + '#options' => $instrument_family_options, + '#default_value' => $selected_family, + // Bind an ajax callback to the change event (which is the default for the + // select form type) of the first dropdown. It will replace the second + // dropdown when rebuilt. + '#ajax' => [ + // When 'event' occurs, Drupal will perform an ajax request in the + // background. Usually the default value is sufficient (eg. change for + // select elements), but valid values include any jQuery event, + // most notably 'mousedown', 'blur', and 'submit'. + 'callback' => '::instrumentDropdownCallback', + 'wrapper' => 'instrument-fieldset-container', + ], + ]; + // Since we don't know if the user has js or not, we always need to output + // this element, then hide it with with css if javascript is enabled. + $form['instrument_family_fieldset']['choose_family'] = [ + '#type' => 'submit', + '#value' => $this->t('Choose'), + '#attributes' => ['class' => ['ajax-example-hide', 'ajax-example-inline']], + ]; + // We are using the path parameter $nojs to signal when to simulate the + // the user turning off JavaScript. We'll remove all the AJAX elements. This + // is not required, and is here so that we can demonstrate a graceful + // fallback without having to turn off JavaScript. + if ($nojs == 'nojs') { + // Removing the #ajax element tells the system not to use AJAX. + unset($form['instrument_family_fieldset']['instrument_family_dropdown']['#ajax']); + // Removing the ajax-example-hide class from the Choose button ensures + // that our JavaScript won't hide it. + unset($form['instrument_family_fieldset']['choose_family']['#attributes']); + } + + // Since we're managing state for this whole fieldset (both the dropdown + // and enabling the Submit button), we want to replace the whole thing + // on AJAX requests. That's why we put it in this container. + $form['instrument_fieldset_container'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'instrument-fieldset-container'], + ]; + // Build the instrument field set. + $form['instrument_fieldset_container']['instrument_fieldset'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Choose an instrument'), + ]; + $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown'] = [ + '#type' => 'select', + '#title' => $instrument_family_options[$selected_family] . ' ' . $this->t('Instruments'), + // When the form is rebuilt during ajax processing, the $selected_family + // variable will now have the new value and so the options will change. + '#options' => static::getSecondDropdownOptions($selected_family), + '#default_value' => !empty($form_state->getValue('instrument_dropdown')) ? $form_state->getValue('instrument_dropdown') : '', + ]; + $form['instrument_fieldset_container']['instrument_fieldset']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + // We might normally use #state to disable the instrument fields based on + // the instrument family fields. But since the premise is that we don't have + // JavaScript running, #state won't work either. We have to set up the state + // of the instrument fieldset here, based on the selected instrument family. + if ($selected_family == 'none') { + $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown']['#title'] = + $this->t('You must choose an instrument family.'); + $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown']['#disabled'] = TRUE; + $form['instrument_fieldset_container']['instrument_fieldset']['submit']['#disabled'] = TRUE; + } + else { + $form['instrument_fieldset_container']['instrument_fieldset']['instrument_dropdown']['#disabled'] = FALSE; + $form['instrument_fieldset_container']['instrument_fieldset']['submit']['#disabled'] = FALSE; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $trigger = (string) $form_state->getTriggeringElement()['#value']; + switch ($trigger) { + case 'Submit': + // Submit: We're done. + drupal_set_message($this->t('Your values have been submitted. Instrument family: @family, Instrument: @instrument', [ + '@family' => $form_state->getValue('instrument_family_dropdown'), + '@instrument' => $form_state->getValue('instrument_dropdown'), + ])); + return; + } + // 'Choose' or anything else will cause rebuild of the form and present + // it again. + $form_state->setRebuild(); + } + + /** + * Provide a new dropdown based on the AJAX call. + * + * This callback will occur *after* the form has been rebuilt by buildForm(). + * Since that's the case, the form should contain the right values for + * instrument_dropdown. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The portion of the render structure that will replace the + * instrument-dropdown-replace form element. + */ + public function instrumentDropdownCallback(array $form, FormStateInterface $form_state) { + return $form['instrument_fieldset_container']; + } + + /** + * Helper function to populate the first dropdown. + * + * This would normally be pulling data from the database. + * + * @return array + * Dropdown options. + */ + public static function getFirstDropdownOptions() { + return [ + 'none' => 'none', + 'String' => 'String', + 'Woodwind' => 'Woodwind', + 'Brass' => 'Brass', + 'Percussion' => 'Percussion', + ]; + } + + /** + * Helper function to populate the second dropdown. + * + * This would normally be pulling data from the database. + * + * @param string $key + * This will determine which set of options is returned. + * + * @return array + * Dropdown options + */ + public static function getSecondDropdownOptions($key = '') { + switch ($key) { + case 'String': + $options = [ + 'Violin' => 'Violin', + 'Viola' => 'Viola', + 'Cello' => 'Cello', + 'Double Bass' => 'Double Bass', + ]; + break; + + case 'Woodwind': + $options = [ + 'Flute' => 'Flute', + 'Clarinet' => 'Clarinet', + 'Oboe' => 'Oboe', + 'Bassoon' => 'Bassoon', + ]; + break; + + case 'Brass': + $options = [ + 'Trumpet' => 'Trumpet', + 'Trombone' => 'Trombone', + 'French Horn' => 'French Horn', + 'Euphonium' => 'Euphonium', + ]; + break; + + case 'Percussion': + $options = [ + 'Bass Drum' => 'Bass Drum', + 'Timpani' => 'Timpani', + 'Snare Drum' => 'Snare Drum', + 'Tambourine' => 'Tambourine', + ]; + break; + + default: + $options = ['none' => 'none']; + break; + } + return $options; + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/DynamicFormSections.php b/sites/all/modules/examples/examples/ajax_example/src/Form/DynamicFormSections.php new file mode 100644 index 000000000..826edd703 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/src/Form/DynamicFormSections.php @@ -0,0 +1,222 @@ + + $this->t('

Like other examples in this module, this form has a path that + can be modified with /nojs to simulate its behavior without JavaScript. +

    +
  • @try_it_without_ajax
  • +
  • @try_it_with_ajax
  • +
', + [ + '@try_it_without_ajax' => Link::createFromRoute( + $this->t('Try it without AJAX'), + 'ajax_example.dynamic_form_sections', ['nojs' => 'nojs']) + ->toString(), + '@try_it_with_ajax' => Link::createFromRoute( + $this->t('Try it with AJAX'), + 'ajax_example.dynamic_form_sections') + ->toString(), + ] + ), + ]; + + $form['question_type_select'] = [ + // This is our select dropdown. + '#type' => 'select', + '#title' => t('Question style'), + // We have a variety of form items you can use to get input from the user. + '#options' => [ + 'Choose question style' => 'Choose question style', + 'Multiple Choice' => 'Multiple Choice', + 'True/False' => 'True/False', + 'Fill-in-the-blanks' => 'Fill-in-the-blanks', + ], + // The #ajax section tells the AJAX system that whenever this dropdown + // emits an event, it should call the callback and put the resulting + // content into the wrapper we specify. The questions-fieldset-wrapper is + // defined below. + '#ajax' => [ + 'wrapper' => 'questions-fieldset-wrapper', + 'callback' => '::promptCallback', + ], + ]; + // The CSS for this module hides this next button if JS is enabled. + $form['question_type_submit'] = [ + '#type' => 'submit', + '#value' => t('Choose'), + '#attributes' => ['class' => ['ajax-example-inline']], + // No need to validate when submitting this. + '#limit_validation_errors' => [], + '#validate' => [], + ]; + + // This section allows us to demonstrate no-AJAX use without turning off + // javascript in the browser. + if ($nojs != 'nojs') { + // Allow JavaScript to hide the choose button if we're using AJAX. + $form['question_type_submit']['#attributes']['class'][] = 'ajax-example-hide'; + } + else { + // Remove #ajax from the above, so it won't perform AJAX behaviors. + unset($form['question_type_select']['#ajax']); + } + + // This fieldset just serves as a container for the part of the form + // that gets rebuilt. It has a nice line around it so you can see it. + $form['questions_fieldset'] = [ + '#type' => 'details', + '#title' => $this->t('Stuff will appear here'), + '#open' => TRUE, + // We set the ID of this fieldset to questions-fieldset-wrapper so the + // AJAX command can replace it. + '#attributes' => ['id' => 'questions-fieldset-wrapper'], + ]; + + // When the AJAX request comes in, or when the user hit 'Submit' if there is + // no JavaScript, the form state will tell us what the user has selected + // from the dropdown. We can look at the value of the dropdown to determine + // which secondary form to display. + $question_type = $form_state->getValue('question_type_select'); + if (!empty($question_type) && $question_type !== 'Choose question style') { + + $form['questions_fieldset']['question'] = [ + '#markup' => t('Who was the first president of the U.S.?'), + ]; + + // Build up a secondary form, based on the type of question the user + // chose. + switch ($question_type) { + case 'Multiple Choice': + $form['questions_fieldset']['question'] = [ + '#type' => 'radios', + '#title' => t('Who was the first president of the United States'), + '#options' => [ + 'George Bush' => 'George Bush', + 'Adam McGuire' => 'Adam McGuire', + 'Abraham Lincoln' => 'Abraham Lincoln', + 'George Washington' => 'George Washington', + ], + + ]; + break; + + case 'True/False': + $form['questions_fieldset']['question'] = [ + '#type' => 'radios', + '#title' => $this->t('Was George Washington the first president of the United States?'), + '#options' => [ + 'George Washington' => 'True', + 0 => 'False', + ], + '#description' => $this->t('Click "True" if you think George Washington was the first president of the United States.'), + ]; + break; + + case 'Fill-in-the-blanks': + $form['questions_fieldset']['question'] = [ + '#type' => 'textfield', + '#title' => $this->t('Who was the first president of the United States'), + '#description' => $this->t('Please type the correct answer to the question.'), + ]; + break; + } + + $form['questions_fieldset']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit your answer'), + ]; + } + return $form; + } + + /** + * Final submit handler. + * + * Reports what values were finally set. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // This is only executed when a button is pressed, not when the AJAXfield + // select is changed. + // Now handle the case of the next, previous, and submit buttons. + // Only submit will result in actual submission, all others rebuild. + if ($form_state->getValue('question_type_submit') == 'Choose') { + $form_state->setValue('question_type_select', $form_state->getUserInput()['question_type_select']); + $form_state->setRebuild(); + } + + if ($form_state->getValue('submit') == 'Submit your answer') { + $form_state->setRebuild(FALSE); + $answer = $form_state->getValue('question'); + // Special handling for the checkbox. + if ($answer == 1 && $form['questions_fieldset']['question']['#type'] == 'checkbox') { + $answer = $form['questions_fieldset']['question']['#title']; + } + if ($answer == $this->t('George Washington')) { + drupal_set_message($this->t('You got the right answer: @answer', ['@answer' => $answer])); + } + else { + drupal_set_message($this->t('Sorry, your answer (@answer) is wrong', ['@answer' => $answer])); + } + return; + } + // Sets the form to be rebuilt after processing. + $form_state->setRebuild(); + } + + /** + * Callback for the select element. + * + * Since the questions_fieldset part of the form has already been built during + * the AJAX request, we can return only that part of the form to the AJAX + * request, and it will insert that part into questions-fieldset-wrapper. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The form structure. + */ + public function promptCallback(array $form, FormStateInterface $form_state) { + return $form['questions_fieldset']; + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/EntityAutocomplete.php b/sites/all/modules/examples/examples/ajax_example/src/Form/EntityAutocomplete.php new file mode 100644 index 000000000..53b0162cd --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/src/Form/EntityAutocomplete.php @@ -0,0 +1,126 @@ +get('entity_type.manager') + ); + $form->setStringTranslation($container->get('string_translation')); + return $form; + } + + /** + * Constructor. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ajax_example_autocomplete_user'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['info'] = [ + '#markup' => '
' . t("This example uses the entity_autocomplete form " + . "element to select users. You'll need a few users on your system for " + . "it to make sense.") . '
', + ]; + + // Here we use the delightful entity_autocomplete form element. It allows us + // to consistently select entities. See https://www.drupal.org/node/2418529. + $form['users'] = [ + // A type of entity_autocomplete lets Drupal know it should autocomplete + // entities. + '#type' => 'entity_autocomplete', + // We can specify entity types to autocomplete. + '#target_type' => 'user', + // Specifying #tags as TRUE allows for multiple selections, separated by + // commas. + '#tags' => TRUE, + '#title' => t('Choose a user. Separate with commas.'), + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + * + * Here we validate and signal an error if there are no users selected. + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $state_users = $form_state->getValue('users'); + if (empty($state_users)) { + $form_state->setErrorByName('users', 'There were no users selected.'); + } + } + + /** + * {@inheritdoc} + * + * On submit, show the user the names of the users they selected. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $state_users = $form_state->getValue('users'); + $users = []; + foreach ($state_users as $state_user) { + $uid = $state_user['target_id']; + $users[] = $this->entityTypeManager->getStorage('user')->load($uid)->getUsername(); + } + drupal_set_message('These are your users: ' . implode(' ', $users)); + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/Simplest.php b/sites/all/modules/examples/examples/ajax_example/src/Form/Simplest.php new file mode 100644 index 000000000..951a7f9cc --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/src/Form/Simplest.php @@ -0,0 +1,80 @@ + $this->t("Choose something and explain why"), + '#type' => 'select', + '#options' => [ + 'one' => 'one', + 'two' => 'two', + 'three' => 'three', + ], + '#ajax' => [ + // #ajax has two required keys: callback and wrapper. + // 'callback' is a function that will be called when this element + // changes. + 'callback' => '::promptCallback', + // 'wrapper' is the HTML id of the page element that will be replaced. + 'wrapper' => 'replace-textfield-container', + ], + ]; + + // The 'replace-textfield-container' container will be replaced whenever + // 'changethis' is updated. + $form['replace_textfield_container'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'replace-textfield-container'], + ]; + $form['replace_textfield_container']['replace_textfield'] = [ + '#type' => 'textfield', + '#title' => $this->t("Why"), + ]; + + // An AJAX request calls the form builder function for every change. + // We can change how we build the form based on $form_state. + $value = $form_state->getValue('changethis'); + // The getValue() method returns NULL by default if the form element does + // not exist. It won't exist yet if we're building it for the first time. + if ($value !== NULL) { + $form['replace_textfield_container']['replace_textfield']['#description'] = + $this->t("Say why you chose '@value'", ['@value' => $value]); + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // No-op. Our form doesn't need a submit handler, because the form is never + // submitted. We add the method here so we fulfill FormInterface. + } + + /** + * Handles switching the available regions based on the selected theme. + */ + public function promptCallback($form, FormStateInterface $form_state) { + return $form['replace_textfield_container']; + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/SubmitDriven.php b/sites/all/modules/examples/examples/ajax_example/src/Form/SubmitDriven.php new file mode 100644 index 000000000..8e99f134d --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/src/Form/SubmitDriven.php @@ -0,0 +1,73 @@ + 'container', + '#attributes' => ['id' => 'box-container'], + ]; + // The box contains some markup that we can change on a submit request. + $form['container']['box'] = [ + '#type' => 'markup', + '#markup' => '

Initial markup for box

', + ]; + + $form['submit'] = [ + '#type' => 'submit', + // The AJAX handler will call our callback, and will replace whatever page + // element has id box-container. + '#ajax' => [ + 'callback' => '::promptCallback', + 'wrapper' => 'box-container', + ], + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + + /** + * Callback for submit_driven example. + * + * Select the 'box' element, change the markup in it, and return it as a + * renderable array. + * + * @return array + * Renderable array (the box element) + */ + public function promptCallback(array &$form, FormStateInterface $form_state) { + // In most cases, it is recommended that you put this logic in form + // generation rather than the callback. Submit driven forms are an + // exception, because you may not want to return the form at all. + $element = $form['container']; + $element['box']['#markup'] = "Clicked submit ({$form_state->getValue('op')}): " . date('c'); + return $element; + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/src/Form/Wizard.php b/sites/all/modules/examples/examples/ajax_example/src/Form/Wizard.php new file mode 100644 index 000000000..acf1f68e7 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/src/Form/Wizard.php @@ -0,0 +1,229 @@ +t('examples/ajax-example/wizard-nojs'), $url) + ->toString(); + + // Prepare link for multiple arguments. + $urltwo = Url::fromUri('internal:/examples/ajax-example/wizard'); + $linktwo = Link::fromTextAndUrl($this->t('examples/ajax-example/wizard'), $urltwo) + ->toString(); + + // We want to deal with hierarchical form values. + $form['#tree'] = TRUE; + $form['description'] = [ + '#markup' => t('This example is a step-by-step wizard. The @link does it without page reloads; the @link1 is the same code but simulates a non-javascript environment, showing it with page reloads.', [ + '@link' => $linktwo, + '@link1' => $link, + ]), + ]; + + $form['step'] = [ + '#type' => 'hidden', + '#value' => !empty($form_state->getValue('step')) ? $form_state->getValue('step') : 1, + ]; + print_r($form_state->getValue('step')); + + if ($form['step']['#value'] == 1) { + $form['step1'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Step 1: Personal details'), + ]; + $form['step1']['name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Your name'), + '#default_value' => empty($form_state->getValue([ + 'step1', + 'name', + ]) ? '' : $form_state->getValue(['step1', 'name'])), + '#required' => TRUE, + ]; + + $form['next'] = [ + '#type' => 'submit', + '#value' => $this->t('Next step'), + '#ajax' => [ + 'wrapper' => 'ajax-example-wizard', + 'callback' => '::prompt', + ], + ]; + } + + // This simply allows us to demonstrate no-javascript use without + // actually turning off javascript in the browser. Removing the #ajax + // element turns off AJAX behaviors on that element and as a result + // ajax.js doesn't get loaded. + // For demonstration only! You don't need this. + if ($no_js_use) { + // Remove the #ajax from the above, so ajax.js won't be loaded. + // For demonstration only. + unset($form['next']['#ajax']); + unset($form['prev']['#ajax']); + } + + return $form; + } + + /** + * Wizard callback function. + * + * @param array $form + * Form API form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form API form. + * + * @return array + * Form array. + */ + public function prompt(array $form, FormStateInterface $form_state) { + return $form; + } + + /** + * Save away the current information. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getTriggeringElement()['#value'] == $this->t('Submit your information')) { + $value_message = $this->t('Your information has been submitted:') . ' '; + foreach ($form_state->getValue('value') as $step => $values) { + $value_message .= "$step: "; + foreach ($values as $key => $value) { + $value_message .= "$key=$value, "; + } + } + drupal_set_message($value_message); + $form_state->setRebuild(FALSE); + // Redirect to #action, else return. + return; + } + else { + $step = $form_state->getValue('step'); + // Increment or decrement the step as needed. Recover values if they + // exist. + if ($form_state->getTriggeringElement()['#value']->__toString() == $this->t('Next step')) { + $step++; + } + elseif ($form_state->getTriggeringElement()['#value']->__toString() == $this->t('Previous step')) { + $step--; + } + + switch ($step) { + case 1: + $form['step1'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Step 1: Personal details'), + ]; + $form['step1']['name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Your name'), + '#default_value' => empty($form_state->getValue([ + 'step1', + 'name', + ]) ? '' : $form_state->getValue(['step1', 'name'])), + '#required' => TRUE, + ]; + $form_state->setValue('step', 1); + break; + + case 2: + unset($form['step1']); + unset($form['next']); + $form['step2'] = [ + '#type' => 'fieldset', + '#title' => t('Step 2: Street address info'), + ]; + $form['step2']['address'] = [ + '#type' => 'textfield', + '#title' => $this->t('Your street address'), + '#default_value' => empty($form_state->getValue([ + 'step2', + 'address', + ]) ? '' : $form_state->getValue(['step2', 'address'])), + '#required' => TRUE, + ]; + $form_state->setValue('step', $step); + break; + + case 3: + + $form['step3'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Step 3: City info'), + ]; + $form['step3']['city'] = [ + '#type' => 'textfield', + '#title' => $this->t('Your city'), + '#default_value' => empty($form_state->getValue([ + 'step3', + 'city', + ]) ? '' : $form_state->getValue(['step3', 'city'])), + '#required' => TRUE, + ]; + $form_state->setValue('step', $step); + break; + } + if ($step == 3) { + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t("Submit your information"), + ]; + } + if ($step > 1 && !isset($form['prev'])) { + $form['prev'] = [ + '#type' => 'submit', + '#value' => t("Previous step"), + // Since all info will be discarded, don't validate on 'prev'. + '#limit_validation_errors' => [], + // #submit is required to use #limit_validation_errors. + '#submit' => ['ajax_example_wizard_submit'], + '#ajax' => [ + 'wrapper' => 'ajax-example-wizard', + 'callback' => '::prompt', + ], + ]; + } + if ($step < 3 && !isset($form['next'])) { + $form['next'] = [ + '#type' => 'submit', + '#value' => $this->t('Next step'), + '#limit_validation_errors' => [], + '#ajax' => [ + 'wrapper' => 'ajax-example-wizard', + 'callback' => '::prompt', + ], + ]; + } + $response = new AjaxResponse(); + $response->addCommand(new HtmlCommand('#ajax-example-wizard', $form)); + return $response; + } + + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/templates/description.html.twig b/sites/all/modules/examples/examples/ajax_example/templates/description.html.twig new file mode 100644 index 000000000..76ed8e5f8 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/templates/description.html.twig @@ -0,0 +1,29 @@ +{# + +Description text for the Ajax Example. + +#} + +{% set simple_ajax_example = path('ajax_example.simplest') %} +{% set ajax_generate_textfields = path('ajax_example.autotextfields') %} +{% set ajax_submit = path('ajax_example.submit_driven_ajax') %} +{% set ajax_dependent_dropdown = path('ajax_example.dependent_dropdown') %} +{% set ajax_dependent_dropdown_nojs = path('ajax_example.dependent_dropdown', {'nojs': 'nojs'}) %} +{% set ajax_dynamic_form = path('ajax_example.dynamic_form_sections') %} +{% set ajax_dynamic_form_nojs = path('ajax_example.dynamic_form_sections', {'nojs': 'nojs'}) %} +{% set ajax_wizard_example = path('ajax_example.wizard') %} +{% set ajax_wizard_example_nojs = path('ajax_example.wizardnojs') %} + +{% trans %} + +

The AJAX example module provides many examples of AJAX including forms, links, and AJAX commands.

+

Simplest AJAX Example

+

Generate textfields

+

Submit-driven AJAX

+

Dependent dropdown

+

Dependent dropdown w/ no JS

+

Dynamic form sections

+

Dynamic form sections w/ no JS

+

AJAX Wizard Example

+

AJAX Wizard Example w/JS turned off

+{% endtrans %} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php new file mode 100644 index 000000000..4528cc411 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/AjaxExampleMenuTest.php @@ -0,0 +1,79 @@ +drupalLogin( + $this->createUser(['access content', 'access user profiles']) + ); + + $assertion = $this->assertSession(); + + // Routes with menu links, and their form buttons. + $routes_with_menu_links = [ + 'ajax_example.description' => [], + 'ajax_example.simplest' => [], + 'ajax_example.autotextfields' => ['Click Me'], + 'ajax_example.submit_driven_ajax' => ['Submit'], + 'ajax_example.dependent_dropdown' => ['Submit'], + 'ajax_example.dynamic_form_sections' => ['Choose'], + 'ajax_example.wizard' => ['Next step'], + 'ajax_example.wizardnojs' => ['Next step'], + 'ajax_example.ajax_link_render' => [], + 'ajax_example.autocomplete_user' => ['Submit'], + ]; + + // Ensure the links appear in the tools menu sidebar. + $this->drupalGet(''); + foreach (array_keys($routes_with_menu_links) as $route) { + $assertion->linkByHrefExists(Url::fromRoute($route)->getInternalPath()); + } + + // All our routes with their form buttons. + $routes = [ + 'ajax_example.ajax_link_callback' => [], + ]; + + // Go to all the routes and click all the buttons. + $routes = array_merge($routes_with_menu_links, $routes); + foreach ($routes as $route => $buttons) { + $url = Url::fromRoute($route); + $this->drupalGet($url); + $assertion->statusCodeEquals(200); + foreach ($buttons as $button) { + $this->drupalPostForm($url, [], $button); + $assertion->statusCodeEquals(200); + } + } + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DependentDropdownTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DependentDropdownTest.php new file mode 100644 index 000000000..11c131d34 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DependentDropdownTest.php @@ -0,0 +1,74 @@ +getSession(); + $assert = $this->assertSession(); + $page = $session->getPage(); + + // Get a URL object for the form, specifying no JS. + $dropdown_url = Url::fromRoute('ajax_example.dependent_dropdown', ['nojs' => 'nojs']); + + // Get the form. + $this->drupalGet($dropdown_url); + // Check for the initial state. + $assert->fieldDisabled('instrument_dropdown'); + $assert->fieldValueEquals('instrument_dropdown', 'none'); + $submit_button = $page->findButton('edit-submit'); + $this->assertTrue($submit_button->hasAttribute('disabled')); + + // Run through the matrix of form submissions. + $families = [ + 'String' => ['Violin', 'Viola', 'Cello', 'Double Bass'], + 'Woodwind' => ['Flute', 'Clarinet', 'Oboe', 'Bassoon'], + 'Brass' => ['Trumpet', 'Trombone', 'French Horn', 'Euphonium'], + 'Percussion' => ['Bass Drum', 'Timpani', 'Snare Drum', 'Tambourine'], + ]; + + foreach ($families as $family => $instruments) { + // Post the form for the instrument family. + $this->drupalPostForm($dropdown_url, ['instrument_family_dropdown' => $family], 'Choose'); + // Get the instrument dropdown elements. + $instrument_options = $page->findAll('css', '#edit-instrument-dropdown option'); + $this->assertCount(count($instruments), $instrument_options); + // Make sure all the instruments are in the select dropdown. + foreach ($instrument_options as $instrument) { + $this->assertContains($instrument->getAttribute('value'), $instruments); + } + // Post each instrument. We have to 'choose' again in order to unlock the + // instrument dropdown. + foreach ($instruments as $instrument) { + $this->drupalPostForm($dropdown_url, ['instrument_family_dropdown' => $family], 'Choose'); + $this->drupalPostForm(NULL, ['instrument_dropdown' => $instrument], 'Submit'); + $assert->pageTextContains("Your values have been submitted. Instrument family: $family, Instrument: $instrument"); + } + } + + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DynamicFormSectionsTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DynamicFormSectionsTest.php new file mode 100644 index 000000000..6403aa091 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/Functional/DynamicFormSectionsTest.php @@ -0,0 +1,70 @@ +assertSession(); + $page = $this->getSession()->getPage(); + + // Get a URL object for the form, specifying no JS. + $dropdown_url = Url::fromRoute('ajax_example.dynamic_form_sections', ['nojs' => 'nojs']); + + // Get the form. + $this->drupalGet($dropdown_url); + // Check for the initial state. + $detail_children = $page->findAll('css', 'div.details-wrapper *'); + $this->assertEmpty($detail_children); + + // Go through the dropdown options. First outlier is 'Choose question style' + // which should have an empty details section. + $this->drupalPostForm($dropdown_url, ['question_type_select' => 'Choose question style'], 'Choose'); + $detail_children = $page->findAll('css', 'div.details-wrapper *'); + $this->assertEmpty($detail_children); + + // Cycle through the other dropdown values. + $question_styles = [ + 'Multiple Choice', + 'True/False', + 'Fill-in-the-blanks', + ]; + // These all add stuff to the details wrapper. + foreach ($question_styles as $question_style) { + $this->drupalPostForm($dropdown_url, ['question_type_select' => $question_style], 'Choose'); + $detail_children = $page->findAll('css', 'div.details-wrapper *'); + $this->assertNotEmpty($detail_children); + $this->drupalPostForm(NULL, ['question' => 'George Washington'], 'Submit your answer'); + $assert->pageTextContains('You got the right answer: George Washington'); + } + // One wrong answer to exercise that code path. + $this->drupalPostForm($dropdown_url, ['question_type_select' => 'Multiple Choice'], 'Choose'); + $detail_children = $page->findAll('css', 'div.details-wrapper *'); + $this->assertNotEmpty($detail_children); + $this->drupalPostForm(NULL, ['question' => 'Abraham Lincoln'], 'Submit your answer'); + $assert->pageTextContains('Sorry, your answer (Abraham Lincoln) is wrong'); + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/AutotextfieldsTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/AutotextfieldsTest.php new file mode 100644 index 000000000..44e82ab43 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/AutotextfieldsTest.php @@ -0,0 +1,80 @@ +getSession(); + $page = $session->getPage(); + $assert = $this->assertSession(); + + // Get the page. + $form_url = Url::fromRoute('ajax_example.autotextfields'); + $this->drupalGet($form_url); + + // Check our initial state. + $assert->checkboxNotChecked('ask_first_name'); + $assert->checkboxNotChecked('ask_last_name'); + $assert->fieldNotExists('first_name'); + $assert->fieldNotExists('last_name'); + // Submit the form. This tests what happens when there are no user + // interactions because drupalPostForm() reloads the form. + $this->drupalPostForm($form_url, [], 'Click Me'); + $assert->pageTextContains('Submit handler: First name: n/a Last name: n/a'); + + // Ask for the first name. + $page->checkField('ask_first_name'); + $assert->assertWaitOnAjaxRequest(); + $assert->fieldExists('first_name'); + $assert->fieldNotExists('last_name'); + // Submit the form. We have to find the field and set its value rather than + // use drupalPostForm(), because when we post the form, it will be rebuilt. + // We are testing the form state after AJAX has modified it, so we must + // preserve that. + $page->findField('first_name')->setValue('Dries'); + $page->pressButton('Click Me'); + $assert->pageTextContains('Submit handler: First name: Dries Last name: n/a'); + + // Ask for the first and last name. + $page->checkField('ask_first_name'); + $assert->assertWaitOnAjaxRequest(); + $assert->fieldExists('first_name'); + $page->checkField('ask_last_name'); + $assert->assertWaitOnAjaxRequest(); + $assert->fieldExists('last_name'); + // Submit the form. + $page->findField('first_name')->setValue('Dries'); + $page->findField('last_name')->setValue('Buytaert'); + $page->pressButton('Click Me'); + $assert->pageTextContains('Submit handler: First name: Dries Last name: Buytaert'); + + // Ask for only the last name. + $page->checkField('ask_last_name'); + $assert->assertWaitOnAjaxRequest(); + $assert->fieldNotExists('first_name'); + $assert->fieldExists('last_name'); + // Submit the form. + $page->findField('last_name')->setValue('Buytaert'); + $page->pressButton('Click Me'); + $assert->pageTextContains('Submit handler: First name: n/a Last name: Buytaert'); + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DependentDropdownTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DependentDropdownTest.php new file mode 100644 index 000000000..8ab642254 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DependentDropdownTest.php @@ -0,0 +1,75 @@ +assertSession(); + $page = $this->getSession()->getPage(); + + // Get a URL object for the form, specifying AJAX. + $dropdown_url = Url::fromRoute('ajax_example.dependent_dropdown', ['nojs' => 'ajax']); + + // Get the form. + $this->drupalGet($dropdown_url); + // Check for the initial state. + $assert->fieldDisabled('instrument_dropdown'); + $assert->fieldValueEquals('instrument_dropdown', 'none'); + $submit_button = $page->findButton('edit-submit'); + $this->assertTrue($submit_button->hasAttribute('disabled')); + + // Run through the matrix of families. + $families = [ + 'String' => ['Violin', 'Viola', 'Cello', 'Double Bass'], + 'Woodwind' => ['Flute', 'Clarinet', 'Oboe', 'Bassoon'], + 'Brass' => ['Trumpet', 'Trombone', 'French Horn', 'Euphonium'], + 'Percussion' => ['Bass Drum', 'Timpani', 'Snare Drum', 'Tambourine'], + ]; + + foreach ($families as $family => $instruments) { + // Select a family. + $family_dropdown = $assert->fieldExists('instrument_family_dropdown'); + $family_dropdown->setValue($family); + $assert->assertWaitOnAjaxRequest(); + + // Get the instrument dropdown elements. + $instrument_options = $page->findAll('css', 'select[name="instrument_dropdown"] option'); + $this->assertCount(count($instruments), $instrument_options); + // Make sure all the instruments are in the select dropdown. + foreach ($instrument_options as $instrument) { + $this->assertContains($instrument->getAttribute('value'), $instruments); + } + + // Post each instrument. + foreach ($instruments as $instrument) { + $this->drupalGet($dropdown_url); + $family_dropdown->setValue($family); + $assert->assertWaitOnAjaxRequest(); + $this->drupalPostForm(NULL, ['instrument_dropdown' => $instrument], 'Submit'); + $assert->pageTextContains("Your values have been submitted. Instrument family: $family, Instrument: $instrument"); + } + } + + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DynamicFormSectionsTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DynamicFormSectionsTest.php new file mode 100644 index 000000000..539e34ec3 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/DynamicFormSectionsTest.php @@ -0,0 +1,67 @@ +assertSession(); + $page = $this->getSession()->getPage(); + + // Get a URL object for the form, specifying no JS. + $dropdown_url = Url::fromRoute('ajax_example.dynamic_form_sections', ['nojs' => 'ajax']); + + // Get the form. + $this->drupalGet($dropdown_url); + // Check for the initial state. + $this->assertEmpty($page->findAll('css', 'div.details-wrapper *')); + + // Cycle through the other dropdown values. + $question_styles = [ + 'Multiple Choice', + 'True/False', + 'Fill-in-the-blanks', + ]; + + // Check expectations against the details wrapper. + $question_type_dropdown = $page->findField('question_type_select'); + foreach ($question_styles as $question_style) { + $question_type_dropdown->setValue($question_style); + $assert->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($page->findAll('css', 'div.details-wrapper *')); + } + // Prompt to choose question should remove the question. + $question_type_dropdown->setValue('Choose question style'); + $assert->assertWaitOnAjaxRequest(); + $this->assertEmpty($page->findAll('css', 'div.details-wrapper *')); + + // Submit the correct answers. + foreach ($question_styles as $question_style) { + $this->drupalGet($dropdown_url); + $question_type_dropdown->setValue($question_style); + $assert->assertWaitOnAjaxRequest(); + $this->drupalPostForm(NULL, ['question' => 'George Washington'], 'Submit your answer'); + $assert->pageTextContains('You got the right answer: George Washington'); + } + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php new file mode 100644 index 000000000..e1588dc76 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/EntityAutocompleteTest.php @@ -0,0 +1,69 @@ +createUser([], $name); + } + + // Get our various Mink elements. + $assert = $this->assertSession(); + $session = $this->getSession(); + $page = $session->getPage(); + // We'll be using the users field quite a bit, so let's make it a variable. + $users_field_name = 'edit-users'; + + // Get the form. + $this->drupalGet(Url::fromRoute('ajax_example.autocomplete_user')); + // Examine the DOM to make sure our change hasn't happened yet. + $assert->fieldValueEquals($users_field_name, ''); + + // Send an event to the DOM. This will start the autocomplete process. + $autocomplete_field = $page->findField($users_field_name); + $session->getDriver()->keyDown($autocomplete_field->getXpath(), 'b'); + + // Wait for the autocomplete request to complete. + $assert->waitOnAutocomplete(); + + // Examine the DOM to see if our expected change happened. + $results = $page->findAll('css', '.ui-autocomplete li'); + $this->assertCount(2, $results); + foreach ($results as $result) { + $this->assertContains($result->getText(), $names); + } + + // Submit to see if our form processed the user properly. + $this->submitForm([$users_field_name => 'bb, bc'], 'Submit'); + $assert->pageTextContains('These are your users: bb bc'); + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SimplestTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SimplestTest.php new file mode 100644 index 000000000..243b5ea55 --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SimplestTest.php @@ -0,0 +1,60 @@ +getSession(); + $page = $session->getPage(); + $assert = $this->assertSession(); + + // Get the page. + $form_url = Url::fromRoute('ajax_example.simplest'); + $this->drupalGet($form_url); + + // Don't repeat ourselves. This makes it easier if we change the markup + // later. + $description_selector = '#replace-textfield-container div.description'; + + // Check our initial state. + $assert->elementExists('css', '#replace-textfield-container'); + $assert->elementNotExists('css', $description_selector); + + // Select values on the dropdown. Start with three so the change event is + // triggered. + foreach (['three', 'two', 'one'] as $value) { + // Select the dropdown value. + $page->selectFieldOption('changethis', $value); + // Wait for AJAX to happen. + $assert->assertWaitOnAjaxRequest(); + // Assert that the description exists. + $assert->elementExists('css', $description_selector); + // Get the description element from the page. + $prompt_element = $page->find('css', $description_selector); + // Assert that the description element says what we expect it to say. + $this->assertEquals( + "Say why you chose '$value'", + $prompt_element->getText() + ); + } + } + +} diff --git a/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php new file mode 100644 index 000000000..e13d1997c --- /dev/null +++ b/sites/all/modules/examples/examples/ajax_example/tests/src/FunctionalJavascript/SubmitDrivenTest.php @@ -0,0 +1,45 @@ +assertSession(); + // Get the page. + $this->drupalGet(Url::fromRoute('ajax_example.submit_driven_ajax')); + // Examine the DOM to make sure our change hasn't happened yet. + $assert->pageTextNotContains('Clicked submit (Submit):'); + // Submit the form. + $this->submitForm([], 'Submit'); + // Wait on the AJAX request. + $assert->assertWaitOnAjaxRequest(); + // Compare DOM to our expectations. + $assert->pageTextContains('Clicked submit (Submit):'); + } + +} diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.info.yml b/sites/all/modules/examples/examples/batch_example/batch_example.info.yml new file mode 100644 index 000000000..25e8adc7e --- /dev/null +++ b/sites/all/modules/examples/examples/batch_example/batch_example.info.yml @@ -0,0 +1,14 @@ +name: Batch Example +type: module +description: An example outlining how a module can define batch operations. +package: Example modules +# core: 8.x +dependencies: + - examples + - toolbar + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.install b/sites/all/modules/examples/examples/batch_example/batch_example.install new file mode 100644 index 000000000..857b4d767 --- /dev/null +++ b/sites/all/modules/examples/examples/batch_example/batch_example.install @@ -0,0 +1,83 @@ +fetchField(); + // A place to store messages during the run. + $sandbox['messages'] = []; + // Last node read via the query. + $sandbox['current_node'] = -1; + } + + // Process nodes by groups of 10 (arbitrary value). + // When a group is processed, the batch update engine determines + // whether it should continue processing in the same request or provide + // progress feedback to the user and wait for the next request. + $limit = 10; + + // Retrieve the next group of nids. + $query = db_select('node', 'n'); + $query->fields('n', ['nid']); + $result = $query + ->where('n.nid > :nid', [':nid' => $sandbox['current_node']]) + ->range(0, $limit) + ->orderBy('n.nid', 'ASC') + ->execute(); + foreach ($result as $row) { + // Here we actually perform a dummy 'update' on the current node. + $node = db_query('SELECT nid FROM {node} WHERE nid = :nid', [':nid' => $row->nid])->fetchField(); + + // Update our progress information. + $sandbox['progress']++; + $sandbox['current_node'] = $row->nid; + } + + // Set the "finished" status, to tell batch engine whether this function + // needs to run again. If you set a float, this will indicate the progress + // of the batch so the progress bar will update. + $sandbox['#finished'] = ($sandbox['progress'] >= $sandbox['max']) ? TRUE : ($sandbox['progress'] / $sandbox['max']); + + // Set up a per-run message; Make a copy of $sandbox so we can change it. + // This is simply a debugging stanza to illustrate how to capture status + // from each pass through hook_update_N(). + $sandbox_status = $sandbox; + // Don't want them in the output. + unset($sandbox_status['messages']); + $sandbox['messages'][] = t('$sandbox=') . print_r($sandbox_status, TRUE); + + if ($sandbox['#finished']) { + // hook_update_N() may optionally return a string which will be displayed + // to the user. + $final_message = '
  • ' . implode('
  • ', $sandbox['messages']) . "
"; + return t('The batch_example demonstration update did what it was supposed to do: @message', ['@message' => $final_message]); + } +} diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.links.menu.yml b/sites/all/modules/examples/examples/batch_example/batch_example.links.menu.yml new file mode 100644 index 000000000..272cebbf8 --- /dev/null +++ b/sites/all/modules/examples/examples/batch_example/batch_example.links.menu.yml @@ -0,0 +1,5 @@ +# Define default links for this module. +batch_example.form: + title: Batch API Examples + description: Batch examples using Drupal Batch API. + route_name: batch_example.form diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.module b/sites/all/modules/examples/examples/batch_example/batch_example.module new file mode 100644 index 000000000..c7873edc8 --- /dev/null +++ b/sites/all/modules/examples/examples/batch_example/batch_example.module @@ -0,0 +1,133 @@ + $id, '@details' => $operation_details] + ); +} + +/** + * Batch operation for batch 2: five at a time. + * + * This is the function that is called on each operation in batch 2. + * + * After each group of 5 control is returned to the batch API for later + * continuation. + */ +function batch_example_op_2($operation_details, &$context) { + // Use the $context['sandbox'] at your convenience to store the + // information needed to track progression between successive calls. + if (empty($context['sandbox'])) { + $context['sandbox'] = []; + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_node'] = 0; + + // Save node count for the termination message. + $context['sandbox']['max'] = 30; + } + + // Process in groups of 5 (arbitrary value). + // When a group of five is processed, the batch update engine determines + // whether it should continue processing in the same request or provide + // progress feedback to the user and wait for the next request. + // That way even though we're already processing at the operation level + // the operation itself is interruptible. + $limit = 5; + + // Retrieve the next group. + $result = range($context['sandbox']['current_node'] + 1, $context['sandbox']['current_node'] + 1 + $limit); + + foreach ($result as $row) { + // Here we actually perform our dummy 'processing' on the current node. + usleep(20000); + + // Store some results for post-processing in the 'finished' callback. + // The contents of 'results' will be available as $results in the + // 'finished' function (in this example, batch_example_finished()). + $context['results'][] = $row . ' ' . $operation_details; + + // Update our progress information. + $context['sandbox']['progress']++; + $context['sandbox']['current_node'] = $row; + $context['message'] = t('Running Batch "@id" @details', + ['@id' => $row, '@details' => $operation_details] + ); + } + + // Inform the batch engine that we are not finished, + // and provide an estimation of the completion level we reached. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']); + } +} + +/** + * Batch 'finished' callback used by both batch 1 and batch 2. + */ +function batch_example_finished($success, $results, $operations) { + if ($success) { + // Here we could do something meaningful with the results. + // We just display the number of nodes we processed... + drupal_set_message(t('@count results processed.', ['@count' => count($results)])); + drupal_set_message(t('The final result was "%final"', ['%final' => end($results)])); + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + drupal_set_message( + t('An error occurred while processing @operation with arguments : @args', + [ + '@operation' => $error_operation[0], + '@args' => print_r($error_operation[0], TRUE), + ] + ) + ); + } +} + +/** + * @} End of "defgroup batch_example". + */ diff --git a/sites/all/modules/examples/examples/batch_example/batch_example.routing.yml b/sites/all/modules/examples/examples/batch_example/batch_example.routing.yml new file mode 100644 index 000000000..d0cb320eb --- /dev/null +++ b/sites/all/modules/examples/examples/batch_example/batch_example.routing.yml @@ -0,0 +1,7 @@ +batch_example.form: + path: 'examples/batch_example' + defaults: + _form: '\Drupal\batch_example\Form\BatchExampleForm' + _title: 'Demo of batch processing' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/batch_example/src/Form/BatchExampleForm.php b/sites/all/modules/examples/examples/batch_example/src/Form/BatchExampleForm.php new file mode 100644 index 000000000..476fdda63 --- /dev/null +++ b/sites/all/modules/examples/examples/batch_example/src/Form/BatchExampleForm.php @@ -0,0 +1,148 @@ + 'markup', + '#markup' => t('This example offers two different batches. The first does 1000 identical operations, each completed in on run; the second does 20 operations, but each takes more than one run to operate if there are more than 5 nodes.'), + ]; + $form['batch'] = [ + '#type' => 'select', + '#title' => 'Choose batch', + '#options' => [ + 'batch_1' => t('batch 1 - 1000 operations'), + 'batch_2' => t('batch 2 - 20 operations.'), + ], + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => 'Go', + ]; + + return $form; + + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Gather our form value. + $value = $form_state->getValues()['batch']; + // Set the batch, using convenience methods. + $batch = []; + switch ($value) { + case 'batch_1': + $batch = $this->generateBatch1(); + break; + + case 'batch_2': + $batch = $this->generateBatch2(); + break; + } + batch_set($batch); + } + + /** + * Generate Batch 1. + * + * Batch 1 will process one item at a time. + * + * This creates an operations array defining what batch 1 should do, including + * what it should do when it's finished. In this case, each operation is the + * same and by chance even has the same $nid to operate on, but we could have + * a mix of different types of operations in the operations array. + */ + public function generateBatch1() { + $num_operations = 1000; + drupal_set_message(t('Creating an array of @num operations', ['@num' => $num_operations])); + + $operations = []; + // Set up an operations array with 1000 elements, each doing function + // batch_example_op_1. + // Each operation in the operations array means at least one new HTTP + // request, running Drupal from scratch to accomplish the operation. If the + // operation returns with $context['finished'] != TRUE, then it will be + // called again. + // In this example, $context['finished'] is always TRUE. + for ($i = 0; $i < $num_operations; $i++) { + // Each operation is an array consisting of + // - The function to call. + // - An array of arguments to that function. + $operations[] = [ + 'batch_example_op_1', + [ + $i + 1, + t('(Operation @operation)', ['@operation' => $i]), + ], + ]; + } + $batch = [ + 'title' => t('Creating an array of @num operations', ['@num' => $num_operations]), + 'operations' => $operations, + 'finished' => 'batch_example_finished', + ]; + return $batch; + } + + /** + * Generate Batch 2. + * + * Batch 2 will process five items at a time. + * + * This creates an operations array defining what batch 2 should do, including + * what it should do when it's finished. In this case, each operation is the + * same and by chance even has the same $nid to operate on, but we could have + * a mix of different types of operations in the operations array. + */ + public function generateBatch2() { + $num_operations = 20; + + $operations = []; + // 20 operations, each one loads all nodes. + for ($i = 0; $i < $num_operations; $i++) { + $operations[] = [ + 'batch_example_op_2', + [t('(Operation @operation)', ['@operation' => $i])], + ]; + } + $batch = [ + 'operations' => $operations, + 'finished' => 'batch_example_finished', + // @current, @remaining, @total, @percentage, @estimate and @elapsed. + // These placeholders are replaced with actual values in _batch_process(), + // using strtr() instead of t(). The values are determined based on the + // number of operations in the 'operations' array (above), NOT by the + // number of nodes that will be processed. In this example, there are 20 + // operations, so @total will always be 20, even though there are multiple + // nodes per operation. + // Defaults to t('Completed @current of @total.'). + 'title' => t('Processing batch 2'), + 'init_message' => t('Batch 2 is starting.'), + 'progress_message' => t('Processed @current out of @total.'), + 'error_message' => t('Batch 2 has encountered an error.'), + ]; + return $batch; + } + +} diff --git a/sites/all/modules/examples/examples/batch_example/tests/src/functional/BatchExampleWebTest.php b/sites/all/modules/examples/examples/batch_example/tests/src/functional/BatchExampleWebTest.php new file mode 100644 index 000000000..687a330ae --- /dev/null +++ b/sites/all/modules/examples/examples/batch_example/tests/src/functional/BatchExampleWebTest.php @@ -0,0 +1,40 @@ +drupalCreateUser(['access content']); + $this->drupalLogin($web_user); + + // Launch Batch 1. + $this->drupalPostForm('examples/batch_example', ['batch' => 'batch_1'], 'Go'); + // Check that 1000 operations were performed. + $this->assertText('1000 results processed'); + + // Launch Batch 2. + $this->drupalPostForm('examples/batch_example', ['batch' => 'batch_2'], 'Go'); + // Check that 600 operations were performed. + $this->assertText('600 results processed'); + } + +} diff --git a/sites/all/modules/examples/examples/block_example/block_example.info.yml b/sites/all/modules/examples/examples/block_example/block_example.info.yml new file mode 100644 index 000000000..8c03440da --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/block_example.info.yml @@ -0,0 +1,15 @@ +name: Block Example +type: module +description: Demonstrates how a module can define blocks. +package: Example modules +# core: 8.x +dependencies: + - drupal:block + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/block_example/block_example.links.menu.yml b/sites/all/modules/examples/examples/block_example/block_example.links.menu.yml new file mode 100644 index 000000000..4ec1c1ebc --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/block_example.links.menu.yml @@ -0,0 +1,3 @@ +block_example.description: + title: Block Example + route_name: block_example.description diff --git a/sites/all/modules/examples/examples/block_example/block_example.module b/sites/all/modules/examples/examples/block_example/block_example.module new file mode 100644 index 000000000..a5ba84b5b --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/block_example.module @@ -0,0 +1,53 @@ +getPluginDefinition(); + if ((!empty($build['#configuration']['label']) && stristr($build['#configuration']['label'], 'uppercase')) || (!empty($definition['subject']) && stristr($definition['subject'], 'uppercase'))) { + // This will uppercase the block title. + $build['#configuration']['label'] = Unicode::strtoupper($build['#configuration']['label']); + } +} + +/** + * @} End of "defgroup block_example". + */ diff --git a/sites/all/modules/examples/examples/block_example/block_example.routing.yml b/sites/all/modules/examples/examples/block_example/block_example.routing.yml new file mode 100644 index 000000000..975fe5b1d --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/block_example.routing.yml @@ -0,0 +1,7 @@ +block_example.description: + path: '/examples/block-example' + defaults: + _controller: '\Drupal\block_example\Controller\BlockExampleController::description' + _title: 'Block Example' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/block_example/config/schema/block_example.schema.yml b/sites/all/modules/examples/examples/block_example/config/schema/block_example.schema.yml new file mode 100644 index 000000000..0de8cc82e --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/config/schema/block_example.schema.yml @@ -0,0 +1,7 @@ +block.settings.example_configurable_text: + type: block_settings + label: 'Example configurable text block configuration' + mapping: + block_example_string: + type: text + label: 'Block contents' diff --git a/sites/all/modules/examples/examples/block_example/src/Controller/BlockExampleController.php b/sites/all/modules/examples/examples/block_example/src/Controller/BlockExampleController.php new file mode 100644 index 000000000..78333251c --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/src/Controller/BlockExampleController.php @@ -0,0 +1,19 @@ + $this->t('A default value. This block was created at %time', ['%time' => date('c')]), + ]; + } + + /** + * {@inheritdoc} + * + * This method defines form elements for custom block configuration. Standard + * block configuration fields are added by BlockBase::buildConfigurationForm() + * (block title and title visibility) and BlockFormController::form() (block + * visibility settings). + * + * @see \Drupal\block\BlockBase::buildConfigurationForm() + * @see \Drupal\block\BlockFormController::form() + */ + public function blockForm($form, FormStateInterface $form_state) { + $form['block_example_string_text'] = [ + '#type' => 'textarea', + '#title' => $this->t('Block contents'), + '#description' => $this->t('This text will appear in the example block.'), + '#default_value' => $this->configuration['block_example_string'], + ]; + return $form; + } + + /** + * {@inheritdoc} + * + * This method processes the blockForm() form fields when the block + * configuration form is submitted. + * + * The blockValidate() method can be used to validate the form submission. + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['block_example_string'] + = $form_state->getValue('block_example_string_text'); + } + + /** + * {@inheritdoc} + */ + public function build() { + return [ + '#markup' => $this->configuration['block_example_string'], + ]; + } + +} diff --git a/sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleEmptyBlock.php b/sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleEmptyBlock.php new file mode 100644 index 000000000..b14facd0d --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/src/Plugin/Block/ExampleEmptyBlock.php @@ -0,0 +1,30 @@ + t("This block's title is changed to uppercase. Any block title which contains 'uppercase' will also be changed to uppercase."), + ]; + } + +} diff --git a/sites/all/modules/examples/examples/block_example/templates/description.html.twig b/sites/all/modules/examples/examples/block_example/templates/description.html.twig new file mode 100644 index 000000000..e3e188522 --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/templates/description.html.twig @@ -0,0 +1,15 @@ +{# + +Description text for the Block Example. + +#} + +{% set block_admin_page = url('block.admin_display')['#markup'] %} + +{% trans %} + +

The Block Example provides three sample blocks which demonstrate the various + block APIs. To experiment with the blocks, enable and configure them on + the block admin page.

+ +{% endtrans %} diff --git a/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleMenuTest.php b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleMenuTest.php new file mode 100644 index 000000000..e7854403b --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleMenuTest.php @@ -0,0 +1,50 @@ +drupalGet(''); + $this->assertLinkByHref('examples/block-example'); + + $this->drupalGet('examples/block-example'); + $this->assertResponse(200, 'Description page exists.'); + + // Verify that the block admin page link works. + $this->clickLink('the block admin page'); + // Since it links to the admin page, we should get a permissions error and + // not 404. + $this->assertResponse(403); + } + +} diff --git a/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleTest.php b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleTest.php new file mode 100644 index 000000000..fb2c5e897 --- /dev/null +++ b/sites/all/modules/examples/examples/block_example/tests/src/Functional/BlockExampleTest.php @@ -0,0 +1,87 @@ +assertSession(); + + // Create user. + $web_user = $this->drupalCreateUser(['administer blocks']); + // Login the admin user. + $this->drupalLogin($web_user); + + $theme_name = \Drupal::config('system.theme')->get('default'); + + // Verify the blocks are listed to be added. + $this->drupalGet('/admin/structure/block/library/' . $theme_name, ['query' => ['region' => 'content']]); + $assert->pageTextContains('Example: configurable text'); + $assert->pageTextContains('Example: empty block'); + $assert->pageTextContains('Example: uppercase this please'); + + // Define and place blocks. + $settings_configurable = [ + 'label' => 'Configurable text', + 'id' => 'block_example_example_configurable_text', + 'theme' => $theme_name, + ]; + $this->drupalPlaceBlock('example_configurable_text', $settings_configurable); + + $settings_uppercase = [ + 'label' => 'Configurable block to be uppercased', + 'id' => 'block_example_example_uppercased', + 'theme' => $theme_name, + ]; + $this->drupalPlaceBlock('example_uppercase', $settings_uppercase); + + $settings_empty = [ + 'label' => 'Empty block', + 'id' => 'block_example_example_empty', + 'theme' => $theme_name, + ]; + $this->drupalPlaceBlock('example_empty', $settings_empty); + + // Verify that blocks are there. Empty block will not be shown, because it + // holds an empty array. + $this->drupalGet(''); + $assert->pageTextContains($settings_configurable['label']); + $assert->pageTextContains($settings_uppercase['label']); + $assert->pageTextContains(Unicode::strtoupper($settings_uppercase['label'])); + $assert->pageTextNotContains($settings_empty['label']); + + // Change content of configurable text block. + $edit = [ + 'settings[block_example_string_text]' => $this->randomMachineName(), + ]; + $this->drupalPostForm('/admin/structure/block/manage/' . $settings_configurable['id'], $edit, 'Save block'); + $assert->statusCodeEquals(200); + + // Verify that new content is shown. + $this->drupalGet(''); + $assert->statusCodeEquals(200); + $assert->pageTextContains($edit['settings[block_example_string_text]']); + } + +} diff --git a/sites/all/modules/examples/examples/cache_example/cache_example.info.yml b/sites/all/modules/examples/examples/cache_example/cache_example.info.yml new file mode 100644 index 000000000..9bf94e41d --- /dev/null +++ b/sites/all/modules/examples/examples/cache_example/cache_example.info.yml @@ -0,0 +1,14 @@ +name: Cache Example +description: Demonstrates how to use Cache API. +package: Example modules +# core: 8.x +type: module +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/cache_example/cache_example.links.menu.yml b/sites/all/modules/examples/examples/cache_example/cache_example.links.menu.yml new file mode 100644 index 000000000..2e148950d --- /dev/null +++ b/sites/all/modules/examples/examples/cache_example/cache_example.links.menu.yml @@ -0,0 +1,4 @@ +cache_example.description: + title: 'Cache Example' + description: 'Example of Drupal Cache API' + route_name: cache_example.description diff --git a/sites/all/modules/examples/examples/cache_example/cache_example.module b/sites/all/modules/examples/examples/cache_example/cache_example.module new file mode 100644 index 000000000..626606ceb --- /dev/null +++ b/sites/all/modules/examples/examples/cache_example/cache_example.module @@ -0,0 +1,36 @@ +setRequestStack($request_stack); + $this->setStringTranslation($translation); + $this->currentUser = $current_user; + $this->cacheBackend = $cache_backend; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + // Forms that require a Drupal service or a custom service should access + // the service using dependency injection. + // @link https://www.drupal.org/node/2203931. + // Those services are passed in the $container through the static create + // method. + return new static( + $container->get('request_stack'), + $container->get('string_translation'), + $container->get('current_user'), + $container->get('cache.default') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'cron_cache'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Log execution time. + $start_time = microtime(TRUE); + + // Try to load the files count from cache. This function will accept two + // arguments: + // - cache object name (cid) + // - cache bin, the (optional) cache bin (most often a database table) where + // the object is to be saved. + // + // cache_get() returns the cached object or FALSE if object does not exist. + if ($cache = $this->cacheBackend->get('cache_example_files_count')) { + /* + * Get cached data. Complex data types will be unserialized automatically. + */ + $files_count = $cache->data; + } + else { + // If there was no cached data available we have to search filesystem. + // Recursively get all .PHP files from Drupal's core folder. + $files_count = count(file_scan_directory('core', '/.php/')); + + // Since we have recalculated, we now need to store the new data into + // cache. Complex data types will be automatically serialized before + // being saved into cache. + // Here we use the default setting and create an unexpiring cache item. + // See below for an example that creates an expiring cache item. + $this->cacheBackend->set('cache_example_files_count', $files_count, CacheBackendInterface::CACHE_PERMANENT); + } + + $end_time = microtime(TRUE); + $duration = $end_time - $start_time; + + // Format intro message. + $intro_message = '

' . $this->t("This example will search Drupal's core folder and display a count of the PHP files in it.") . ' '; + $intro_message .= $this->t('This can take a while, since there are a lot of files to be searched.') . ' '; + $intro_message .= $this->t('We will search filesystem just once and save output to the cache. We will use cached data for later requests.') . '

'; + $intro_message .= '

' + . $this->t( + 'Reload this page to see cache in action.', + ['@url' => $this->getRequest()->getRequestUri()] + ) + . ' '; + $intro_message .= $this->t('You can use the button below to remove cached data.') . '

'; + + $form['file_search'] = [ + '#type' => 'fieldset', + '#title' => $this->t('File search caching'), + ]; + $form['file_search']['introduction'] = [ + '#markup' => $intro_message, + ]; + + $color = empty($cache) ? 'red' : 'green'; + $retrieval = empty($cache) ? $this->t('calculated by traversing the filesystem') : $this->t('retrieved from cache'); + + $form['file_search']['statistics'] = [ + '#type' => 'item', + '#markup' => $this->t('%count files exist in this Drupal installation; @retrieval in @time ms.
(Source: @source)', [ + '%count' => $files_count, + '@retrieval' => $retrieval, + '@time' => number_format($duration * 1000, 2), + '@color' => $color, + '@source' => empty($cache) ? $this->t('actual file search') : $this->t('cached'), + ] + ), + ]; + $form['file_search']['remove_file_count'] = [ + '#type' => 'submit', + '#submit' => [[$this, 'expireFiles']], + '#value' => $this->t('Explicitly remove cached file count'), + ]; + + $form['expiration_demo'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Cache expiration settings'), + ]; + $form['expiration_demo']['explanation'] = [ + '#markup' => $this->t('A cache item can be set as CACHE_PERMANENT, meaning that it will only be removed when explicitly cleared, or it can have an expiration time (a Unix timestamp).'), + ]; + + $item = $this->cacheBackend->get('cache_example_expiring_item', TRUE); + if ($item == FALSE) { + $item_status = $this->t('Cache item does not exist'); + } + else { + $item_status = $item->valid ? $this->t('Cache item exists and is set to expire at %time', ['%time' => $item->data]) : + $this->t('Cache_item is invalid'); + } + + $form['expiration_demo']['current_status'] = [ + '#type' => 'item', + '#title' => $this->t('Current status of cache item "cache_example_expiring_item"'), + '#markup' => $item_status, + ]; + $form['expiration_demo']['expiration'] = [ + '#type' => 'select', + '#title' => $this->t('Time before cache expiration'), + '#options' => [ + 'never_remove' => $this->t('CACHE_PERMANENT'), + -10 => $this->t('Immediate expiration'), + 10 => $this->t('10 seconds from form submission'), + 60 => $this->t('1 minute from form submission'), + 300 => $this->t('5 minutes from form submission'), + ], + '#default_value' => -10, + '#description' => $this->t('Any cache item can be set to only expire when explicitly cleared, or to expire at a given time.'), + ]; + $form['expiration_demo']['create_cache_item'] = [ + '#type' => 'submit', + '#value' => $this->t('Create a cache item with this expiration'), + '#submit' => [[$this, 'createExpiringItem']], + ]; + + $form['cache_clearing'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Expire and remove options'), + '#description' => $this->t("We have APIs to expire cached items and also to just remove them. Unfortunately, they're all the same API, cache_clear_all"), + ]; + $form['cache_clearing']['cache_clear_type'] = [ + '#type' => 'radios', + '#title' => $this->t('Type of cache clearing to do'), + '#options' => [ + 'expire' => $this->t('Remove items from the "cache" bin that have expired'), + 'remove_all' => $this->t('Remove all items from the "cache" bin regardless of expiration'), + 'remove_tag' => $this->t('Remove all items in the "cache" bin with the tag "cache_example" set to 1'), + ], + '#default_value' => 'expire', + ]; + // Submit button to clear cached data. + $form['cache_clearing']['clear_expired'] = [ + '#type' => 'submit', + '#value' => $this->t('Clear or expire cache'), + '#submit' => [[$this, 'cacheClearing']], + '#access' => $this->currentUser->hasPermission('administer site configuration'), + ]; + + return $form; + } + + /** + * Submit handler that explicitly clears cache_example_files_count from cache. + */ + public function expireFiles($form, &$form_state) { + // Clear cached data. This function will delete cached object from cache + // bin. + // + // The first argument is cache id to be deleted. Since we've provided it + // explicitly, it will be removed whether or not it has an associated + // expiration time. The second argument (required here) is the cache bin. + // Using cache_clear_all() explicitly in this way + // forces removal of the cached item. + $this->cacheBackend->delete('cache_example_files_count'); + + // Display message to the user. + drupal_set_message($this->t('Cached data key "cache_example_files_count" was cleared.'), 'status'); + } + + /** + * Submit handler to create a new cache item with specified expiration. + */ + public function createExpiringItem($form, &$form_state) { + + $tags = [ + 'cache_example:1', + ]; + + $interval = $form_state->getValue('expiration'); + if ($interval == 'never_remove') { + $expiration = CacheBackendInterface::CACHE_PERMANENT; + $expiration_friendly = $this->t('Never expires'); + } + else { + $expiration = time() + $interval; + $expiration_friendly = format_date($expiration); + } + // Set the expiration to the actual Unix timestamp of the end of the + // required interval. Also add a tag to it to be able to clear caches more + // precise. + $this->cacheBackend->set('cache_example_expiring_item', $expiration_friendly, $expiration, $tags); + drupal_set_message($this->t('cache_example_expiring_item was set to expire at %time', ['%time' => $expiration_friendly])); + } + + /** + * Submit handler to demonstrate the various uses of cache_clear_all(). + */ + public function cacheClearing($form, &$form_state) { + switch ($form_state->getValue('cache_clear_type')) { + case 'expire': + // Here we'll remove all cache keys in the 'cache' bin that have + // expired. + $this->cacheBackend->garbageCollection(); + drupal_set_message($this->t('\Drupal::cache()->garbageCollection() was called, removing any expired cache items.')); + break; + + case 'remove_all': + // This removes all keys in a bin using a super-wildcard. This + // has nothing to do with expiration. It's just brute-force removal. + $this->cacheBackend->deleteAll(); + drupal_set_message($this->t('ALL entries in the "cache" bin were removed with \Drupal::cache()->deleteAll().')); + break; + + case 'remove_tag': + // This removes cache entries with the tag "cache_example" set to 1 in + // the "cache". + $tags = [ + 'cache_example:1', + ]; + Cache::invalidateTags($tags); + drupal_set_message($this->t('Cache entries with the tag "cache_example" set to 1 in the "cache" bin were invalidated with \Drupal\Core\Cache\Cache::invalidateTags($tags).')); + break; + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + } + +} diff --git a/sites/all/modules/examples/examples/cache_example/tests/src/Functional/CacheExampleTest.php b/sites/all/modules/examples/examples/cache_example/tests/src/Functional/CacheExampleTest.php new file mode 100644 index 000000000..86b9bc2c4 --- /dev/null +++ b/sites/all/modules/examples/examples/cache_example/tests/src/Functional/CacheExampleTest.php @@ -0,0 +1,110 @@ +assertSession(); + + // Test for a link to the cache_example in the Tools menu. + $this->drupalGet(''); + $assert->statusCodeEquals(200); + + $assert->linkByHrefExists('examples/cache-example'); + + // Verify if the can successfully access the cache_example form. + $this->drupalGet('examples/cache-example'); + $assert->statusCodeEquals(200); + } + + /** + * Test that our caches function. + * + * Does the following: + * - Load cache example page and test if displaying uncached version. + * - Reload once again and test if displaying cached version. + * - Find reload link and click on it. + * - Clear cache at the end and test if displaying uncached version again. + */ + public function testCacheExampleBasic() { + $assert = $this->assertSession(); + + // We need administrative privileges to clear the cache. + $admin_user = $this->drupalCreateUser(['administer site configuration']); + $this->drupalLogin($admin_user); + + // Get initial page cache example page, first time accessed, + // and assert uncached output. + $this->drupalGet('examples/cache-example'); + $assert->pageTextContains('Source: actual file search'); + + // Reload the page; the number should be cached. + $this->drupalGet('examples/cache-example'); + $assert->pageTextContains('Source: cached'); + + // Now push the button to remove the count. + $this->drupalPostForm('examples/cache-example', [], 'Explicitly remove cached file count'); + $assert->pageTextContains('Source: actual file search'); + + // Create a cached item. First make sure it doesn't already exist. + $assert->pageTextContains('Cache item does not exist'); + $this->drupalPostForm('examples/cache-example', ['expiration' => -10], 'Create a cache item with this expiration'); + // We should now have an already-expired item. Automatically invalid. + $assert->pageTextContains('Cache_item is invalid'); + // Now do the expiration operation. + $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'expire'], 'Clear or expire cache'); + // And verify that it was removed. + $assert->pageTextContains('Cache item does not exist'); + + // Create a cached item. This time we'll make it not expire. + $this->drupalPostForm('examples/cache-example', ['expiration' => 'never_remove'], 'Create a cache item with this expiration'); + // We should now have an never-remove item. + $assert->pageTextContains('Cache item exists and is set to expire at Never expires'); + // Now do the expiration operation. + $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'expire'], 'Clear or expire cache'); + // And verify that it was not removed. + $assert->pageTextContains('Cache item exists and is set to expire at Never expires'); + // Now do tag invalidation. + $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'remove_tag'], 'Clear or expire cache'); + // And verify that it was invalidated. + $assert->pageTextContains('Cache_item is invalid'); + // Do the hard delete. + $this->drupalPostForm('examples/cache-example', ['cache_clear_type' => 'remove_all'], 'Clear or expire cache'); + // And verify that it was removed. + $assert->pageTextContains('Cache item does not exist'); + } + +} diff --git a/sites/all/modules/examples/examples/composer.json b/sites/all/modules/examples/examples/composer.json new file mode 100644 index 000000000..26d4063ab --- /dev/null +++ b/sites/all/modules/examples/examples/composer.json @@ -0,0 +1,12 @@ +{ + "name": "drupal/examples", + "description": "The Examples for Developers project aims to provide high-quality, well-documented API examples for a broad range of Drupal core functionality.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/examples", + "support": { + "issues": "https://www.drupal.org/project/issues/examples", + "documentation": "https://api.drupal.org/api/examples", + "source": "http://cgit.drupalcode.org/examples" + }, + "license": "GPL-2.0+" +} diff --git a/sites/all/modules/examples/examples/config_entity_example/config/install/config_entity_example.robot.marvin.yml b/sites/all/modules/examples/examples/config_entity_example/config/install/config_entity_example.robot.marvin.yml new file mode 100644 index 000000000..1a7b1522e --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/config/install/config_entity_example.robot.marvin.yml @@ -0,0 +1,20 @@ +# This file defines a default config entity. This allows the module to include +# config entities that are present 'out of the box'. Default config entities +# are created in Drupal when the module is enabled. They are removed when the +# module is uninstalled. + +# Default config entities can be edited by the user within Drupal. This edited +# entity wil become exportable through the configuration system. This file, +# however, will remain untouched. + +# For our config entity to be added at installation time, we have to place it in +# the config/install directory of our module. Thus our file is located at: +# config/install/config_entity_example.robot.marvin.yml. + +# You can see where these properties are defined in the annotation of +# Drupal\config_entity_example\Entity\Robot. + +# The id of the config entity. +id: marvin +# Our properties follow. +label: 'Marvin, the paranoid android' diff --git a/sites/all/modules/examples/examples/config_entity_example/config/schema/config_entity_example.schema.yml b/sites/all/modules/examples/examples/config_entity_example/config/schema/config_entity_example.schema.yml new file mode 100644 index 000000000..08121765d --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/config/schema/config_entity_example.schema.yml @@ -0,0 +1,28 @@ +# Schema for the configuration files of the Config Entity Example module. + +# This schema tells the config system how to read our config YML files. +# See for example the file config/config_entity_example.robot.marvin.yml, which +# contains our default config entity. + +# Documentation for schema files like this one is located here: +# https://drupal.org/node/1905070 + +config_entity_example.robot.*: + type: mapping + label: 'Robot' + mapping: + id: + type: string + label: 'Robot id' + uuid: + type: string + label: 'UUID' + label: + type: label + label: 'Label' + floopy: + type: boolean + label: 'Floopy' + langcode: + type: string + label: 'Default language' diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.info.yml b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.info.yml new file mode 100644 index 000000000..e517641e5 --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.info.yml @@ -0,0 +1,13 @@ +name: 'Config Entity Example' +type: module +description: 'Demonstrates how to create a config entity type.' +package: Example modules +# core: 8.x +dependencies: + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.action.yml b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.action.yml new file mode 100644 index 000000000..e55bd4af6 --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.action.yml @@ -0,0 +1,14 @@ +# Add some local task links to facilitate navigation. + +config_entity_example.add_action: + route_name: entity.robot.add_form + title: 'Add robot' + appears_on: + - entity.robot.list + +config_entity_example.list_action: + route_name: entity.robot.list + title: 'List robots' + appears_on: + - entity.robot.add_form + - entity.robot.edit_form diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.menu.yml b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.menu.yml new file mode 100644 index 000000000..328da767d --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.links.menu.yml @@ -0,0 +1,3 @@ +config_entity_example.menu: + title: Config Entity Example + route_name: entity.robot.list diff --git a/sites/all/modules/examples/examples/config_entity_example/config_entity_example.module b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.module new file mode 100644 index 000000000..251c65922 --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/config_entity_example.module @@ -0,0 +1,42 @@ +t('Robot'); + $header['machine_name'] = $this->t('Machine Name'); + $header['floopy'] = $this->t('Floopy'); + return $header + parent::buildHeader(); + } + + /** + * Builds a row for an entity in the entity listing. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to build the row. + * + * @return array + * A render array of the table row for displaying the entity. + * + * @see \Drupal\Core\Entity\EntityListController::render() + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['machine_name'] = $entity->id(); + $row['floopy'] = $entity->floopy; + + return $row + parent::buildRow($entity); + } + + /** + * Adds some descriptive text to our entity list. + * + * Typically, there's no need to override render(). You may wish to do so, + * however, if you want to add markup before or after the table. + * + * @return array + * Renderable array. + */ + public function render() { + $build = $this->description(); + $build[] = parent::render(); + return $build; + } + +} diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Entity/Robot.php b/sites/all/modules/examples/examples/config_entity_example/src/Entity/Robot.php new file mode 100644 index 000000000..f9286b030 --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/src/Entity/Robot.php @@ -0,0 +1,89 @@ +t('Create Robot'); + return $actions; + } + +} diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotDeleteForm.php b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotDeleteForm.php new file mode 100644 index 000000000..99bdae753 --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotDeleteForm.php @@ -0,0 +1,89 @@ +t('Are you sure you want to delete robot %label?', [ + '%label' => $this->entity->label(), + ]); + } + + /** + * Gather the confirmation text. + * + * The confirm text is used as the text in the button that confirms the + * question posed by getQuestion(). + * + * @return string + * Translated string. + */ + public function getConfirmText() { + return $this->t('Delete Robot'); + } + + /** + * Gets the cancel URL. + * + * Provides the URL to go to if the user cancels the action. For entity + * delete forms, this is typically the route that points at the list + * controller. + * + * @return \Drupal\Core\Url + * The URL to go to if the user cancels the deletion. + */ + public function getCancelUrl() { + return new Url('entity.robot.list'); + } + + /** + * The submit handler for the confirm form. + * + * For entity delete forms, you use this to delete the entity in + * $this->entity. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Delete the entity. + $this->entity->delete(); + + // Set a message that the entity was deleted. + drupal_set_message($this->t('Robot %label was deleted.', [ + '%label' => $this->entity->label(), + ])); + + // Redirect the user to the list controller when complete. + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotEditForm.php b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotEditForm.php new file mode 100644 index 000000000..eda096f63 --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotEditForm.php @@ -0,0 +1,35 @@ +t('Update Robot'); + return $actions; + } + +} diff --git a/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotFormBase.php b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotFormBase.php new file mode 100644 index 000000000..f6afad05f --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/src/Form/RobotFormBase.php @@ -0,0 +1,221 @@ +entityQueryFactory = $query_factory; + } + + /** + * Factory method for RobotFormBase. + * + * When Drupal builds this class it does not call the constructor directly. + * Instead, it relies on this method to build the new object. Why? The class + * constructor may take multiple arguments that are unknown to Drupal. The + * create() method always takes one parameter -- the container. The purpose + * of the create() method is twofold: It provides a standard way for Drupal + * to construct the object, meanwhile it provides you a place to get needed + * constructor parameters from the container. + * + * In this case, we ask the container for an entity query factory. We then + * pass the factory to our class as a constructor parameter. + */ + public static function create(ContainerInterface $container) { + return new static($container->get('entity.query')); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::form(). + * + * Builds the entity add/edit form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An associative array containing the robot add/edit form. + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get anything we need from the base class. + $form = parent::buildForm($form, $form_state); + + // Drupal provides the entity to us as a class variable. If this is an + // existing entity, it will be populated with existing values as class + // variables. If this is a new entity, it will be a new object with the + // class of our entity. Drupal knows which class to call from the + // annotation on our Robot class. + $robot = $this->entity; + + // Build the form. + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $robot->label(), + '#required' => TRUE, + ]; + $form['id'] = [ + '#type' => 'machine_name', + '#title' => $this->t('Machine name'), + '#default_value' => $robot->id(), + '#machine_name' => [ + 'exists' => [$this, 'exists'], + 'replace_pattern' => '([^a-z0-9_]+)|(^custom$)', + 'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".', + ], + '#disabled' => !$robot->isNew(), + ]; + $form['floopy'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Floopy'), + '#default_value' => $robot->floopy, + ]; + + // Return the form. + return $form; + } + + /** + * Checks for an existing robot. + * + * @param string|int $entity_id + * The entity ID. + * @param array $element + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return bool + * TRUE if this format already exists, FALSE otherwise. + */ + public function exists($entity_id, array $element, FormStateInterface $form_state) { + // Use the query factory to build a new robot entity query. + $query = $this->entityQueryFactory->get('robot'); + + // Query the entity ID to see if its in use. + $result = $query->condition('id', $element['#field_prefix'] . $entity_id) + ->execute(); + + // We don't need to return the ID, only if it exists or not. + return (bool) $result; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::actions(). + * + * To set the submit button text, we need to override actions(). + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * An array of supported actions for the current entity form. + */ + protected function actions(array $form, FormStateInterface $form_state) { + // Get the basic actins from the base class. + $actions = parent::actions($form, $form_state); + + // Change the submit button text. + $actions['submit']['#value'] = $this->t('Save'); + + // Return the result. + return $actions; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::validate(). + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + */ + public function validate(array $form, FormStateInterface $form_state) { + parent::validate($form, $form_state); + + // Add code here to validate your config entity's form elements. + // Nothing to do here. + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::save(). + * + * Saves the entity. This is called after submit() has built the entity from + * the form values. Do not override submit() as save() is the preferred + * method for entity form controllers. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + */ + public function save(array $form, FormStateInterface $form_state) { + // EntityForm provides us with the entity we're working on. + $robot = $this->getEntity(); + + // Drupal already populated the form values in the entity object. Each + // form field was saved as a public variable in the entity class. PHP + // allows Drupal to do this even if the method is not defined ahead of + // time. + $status = $robot->save(); + + // Grab the URL of the new entity. We'll use it in the message. + $url = $robot->urlInfo(); + + // Create an edit link. + $edit_link = Link::fromTextAndUrl($this->t('Edit'), $url)->toString(); + + if ($status == SAVED_UPDATED) { + // If we edited an existing entity... + drupal_set_message($this->t('Robot %label has been updated.', ['%label' => $robot->label()])); + $this->logger('contact')->notice('Robot %label has been updated.', ['%label' => $robot->label(), 'link' => $edit_link]); + } + else { + // If we created a new entity... + drupal_set_message($this->t('Robot %label has been added.', ['%label' => $robot->label()])); + $this->logger('contact')->notice('Robot %label has been added.', ['%label' => $robot->label(), 'link' => $edit_link]); + } + + // Redirect the user back to the listing route after the save operation. + $form_state->setRedirect('entity.robot.list'); + } + +} diff --git a/sites/all/modules/examples/examples/config_entity_example/src/RobotAccessController.php b/sites/all/modules/examples/examples/config_entity_example/src/RobotAccessController.php new file mode 100644 index 000000000..e28b13e8f --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/src/RobotAccessController.php @@ -0,0 +1,34 @@ +The Config Entity Example module defines a Robot entity type. This is a list + of the Robot entities currently in your Drupal site.

By default, when + you enable this module, one entity is created from configuration. This is + why we call them Config Entities. Marvin, the paranoid android, is created + in the database when the module is enabled.

You can view a list of + Robots here. You can also use the 'Operations' column to edit and delete + Robots.

+ +{% endtrans %} diff --git a/sites/all/modules/examples/examples/config_entity_example/tests/src/Functional/ConfigEntityExampleTest.php b/sites/all/modules/examples/examples/config_entity_example/tests/src/Functional/ConfigEntityExampleTest.php new file mode 100644 index 000000000..05e97c8d4 --- /dev/null +++ b/sites/all/modules/examples/examples/config_entity_example/tests/src/Functional/ConfigEntityExampleTest.php @@ -0,0 +1,172 @@ +assertSession(); + + // 1) Verify that the Marvin entity was created when the module was + // installed. + $entity = Robot::load('marvin'); + $this->assertNotNull($entity, 'Marvin was created during installation.'); + + // 2) Verify that permissions are applied to the various defined paths. + // Define some paths. Since the Marvin entity is defined, we can use it + // in our management paths. + $forbidden_paths = [ + '/examples/config-entity-example', + '/examples/config-entity-example/add', + '/examples/config-entity-example/manage/marvin', + '/examples/config-entity-example/manage/marvin/delete', + ]; + // Check each of the paths to make sure we don't have access. At this point + // we haven't logged in any users, so the client is anonymous. + foreach ($forbidden_paths as $path) { + $this->drupalGet($path); + $assert->statusCodeEquals(403); + } + + // Create a user with no permissions. + $noperms_user = $this->drupalCreateUser(); + $this->drupalLogin($noperms_user); + // Should be the same result for forbidden paths, since the user needs + // special permissions for these paths. + foreach ($forbidden_paths as $path) { + $this->drupalGet($path); + $assert->statusCodeEquals(403); + } + + // Create a user who can administer robots. + $admin_user = $this->drupalCreateUser(['administer robots']); + $this->drupalLogin($admin_user); + // Forbidden paths aren't forbidden any more. + foreach ($forbidden_paths as $unforbidden) { + $this->drupalGet($unforbidden); + $assert->statusCodeEquals(200); + } + + // Now that we have the admin user logged in, check the menu links. + $this->drupalGet(''); + $assert->linkByHrefExists('examples/config-entity-example'); + + // 3) Verify that we can manage entities through the user interface. + // We still have the admin user logged in, so we'll create, update, and + // delete an entity. + // Go to the list page. + $this->drupalGet('/examples/config-entity-example'); + $this->clickLink('Add robot'); + $robot_machine_name = 'roboname'; + $this->drupalPostForm( + NULL, + [ + 'label' => $robot_machine_name, + 'id' => $robot_machine_name, + 'floopy' => TRUE, + ], + 'Create Robot' + ); + + // 4) Verify that our robot appears when we edit it. + $this->drupalGet('/examples/config-entity-example/manage/' . $robot_machine_name); + $assert->fieldExists('label'); + $assert->checkboxChecked('edit-floopy'); + + // 5) Verify that the label and machine name are shown in the list. + $this->drupalGet('/examples/config-entity-example'); + $this->clickLink('Add robot'); + $robby_machine_name = 'robby_machine_name'; + $robby_label = 'Robby label'; + $this->drupalPostForm( + NULL, + [ + 'label' => $robby_label, + 'id' => $robby_machine_name, + 'floopy' => TRUE, + ], + 'Create Robot' + ); + $this->drupalGet('/examples/config-entity-example'); + $assert->pageTextContains($robby_label); + $assert->pageTextContains($robby_machine_name); + + // 6) Verify that required links are present on respective paths. + $this->assertLinkByHref('/examples/config-entity-example/add'); + $this->assertLinkByHref('/examples/config-entity-example/manage/robby_machine_name'); + $this->assertLinkByHref('/examples/config-entity-example/manage/robby_machine_name/delete'); + + // Verify links on Add Robot. + $this->drupalGet('/examples/config-entity-example/add'); + $this->assertActionButton('examples/config-entity-example'); + + // Verify links on Edit Robot. + $this->drupalGet('/examples/config-entity-example/manage/robby_machine_name'); + $this->assertLinkByHref('/examples/config-entity-example/manage/robby_machine_name/delete'); + $this->assertActionButton('examples/config-entity-example'); + + // Verify links on Delete Robot. + $this->drupalGet('/examples/config-entity-example/manage/robby_machine_name/delete'); + // List page will be the destination of the cancel link. + $cancel_button = $this->xpath( + '//a[@id="edit-cancel" and contains(@href, :path)]', + [':path' => '/examples/config-entity-example'] + ); + $this->assertEqual(count($cancel_button), 1, 'Found cancel button linking to list page.'); + } + + /** + * Wrap an assertion for the action button. + * + * @param string $path + * Drupal path to a page. + */ + protected function assertActionButton($path) { + $button_element = $this->xpath( + '//a[contains(@class, "button-action") and contains(@data-drupal-link-system-path, :path)]', + [':path' => $path] + ); + $this->assertEqual(count($button_element), 1, 'Found action button for path: ' . $path); + } + +} diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.info.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.info.yml new file mode 100644 index 000000000..914f2720f --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.info.yml @@ -0,0 +1,15 @@ +name: Content Entity Example +type: module +description: Demonstrates how to create a content entity. +package: Example modules +# core: 8.x +# These modules are required by the tests, must be available at bootstrap time +dependencies: + - drupal:options + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.action.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.action.yml new file mode 100644 index 000000000..d1ead9d33 --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.action.yml @@ -0,0 +1,11 @@ +# All action links for this module + +content_entity_example.contact_add: + # Which route will be called by the link + route_name: content_entity_example.contact_add + title: 'Add contact' + + # Where will the link appear, defined by route name. + appears_on: + - entity.content_entity_example_contact.collection + - entity.content_entity_example_contact.canonical diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.menu.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.menu.yml new file mode 100644 index 000000000..6454748ec --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.menu.yml @@ -0,0 +1,12 @@ +# Define the menu links for this module + +entity.content_entity_example_contact.collection: + title: 'Content Entity Example: Contacts listing' + route_name: entity.content_entity_example_contact.collection + description: 'List contacts' + weight: 10 +content_entity_example_contact.admin.structure.settings: + title: 'Contact settings' + description: 'Configure contact entity' + route_name: content_entity_example.contact_settings + parent: system.admin_structure diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.task.yml b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.task.yml new file mode 100644 index 000000000..a76cf585a --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.links.task.yml @@ -0,0 +1,22 @@ +# Define the 'local' links for the module + +contact.settings_tab: + route_name: content_entity_example.contact_settings + title: Settings + base_route: content_entity_example.contact_settings + +contact.view: + route_name: entity.content_entity_example_contact.canonical + base_route: entity.content_entity_example_contact.canonical + title: View + +contact.page_edit: + route_name: entity.content_entity_example_contact.edit_form + base_route: entity.content_entity_example_contact.canonical + title: Edit + +contact.delete_confirm: + route_name: entity.content_entity_example_contact.delete_form + base_route: entity.content_entity_example_contact.canonical + title: Delete + weight: 10 diff --git a/sites/all/modules/examples/examples/content_entity_example/content_entity_example.module b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.module new file mode 100644 index 000000000..ff7e59a63 --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/content_entity_example.module @@ -0,0 +1,45 @@ +. (replace dashes with underscores) + * Example: 'entity.content_entity_example_contact.canonical' + * + * See routing file above for the corresponding implementation + * + * The Contact class defines methods and fields for the contact entity. + * + * Being derived from the ContentEntityBase class, we can override the methods + * we want. In our case we want to provide access to the standard fields about + * creation and changed time stamps. + * + * Our interface (see ContactInterface) also exposes the EntityOwnerInterface. + * This allows us to provide methods for setting and providing ownership + * information. + * + * The most important part is the definitions of the field properties for this + * entity type. These are of the same type as fields added through the GUI, but + * they can by changed in code. In the definition we can define if the user with + * the rights privileges can influence the presentation (view, edit) of each + * field. + * + * The class also uses the EntityChangedTrait trait which allows it to record + * timestamps of save operations. + */ +class Contact extends ContentEntityBase implements ContactInterface { + + use EntityChangedTrait; + + /** + * {@inheritdoc} + * + * When a new entity instance is added, set the user_id entity reference to + * the current user as the creator of the instance. + */ + public static function preCreate(EntityStorageInterface $storage_controller, array &$values) { + parent::preCreate($storage_controller, $values); + $values += [ + 'user_id' => \Drupal::currentUser()->id(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getCreatedTime() { + return $this->get('created')->value; + } + + /** + * {@inheritdoc} + */ + public function getChangedTime() { + return $this->get('changed')->value; + } + + /** + * {@inheritdoc} + */ + public function getOwner() { + return $this->get('user_id')->entity; + } + + /** + * {@inheritdoc} + */ + public function getOwnerId() { + return $this->get('user_id')->target_id; + } + + /** + * {@inheritdoc} + */ + public function setOwnerId($uid) { + $this->set('user_id', $uid); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOwner(UserInterface $account) { + $this->set('user_id', $account->id()); + return $this; + } + + /** + * {@inheritdoc} + * + * Define the field properties here. + * + * Field name, type and size determine the table structure. + * + * In addition, we can define how the field and its content can be manipulated + * in the GUI. The behaviour of the widgets used can be determined here. + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + + // Standard field, used as unique if primary index. + $fields['id'] = BaseFieldDefinition::create('integer') + ->setLabel(t('ID')) + ->setDescription(t('The ID of the Contact entity.')) + ->setReadOnly(TRUE); + + // Standard field, unique outside of the scope of the current project. + $fields['uuid'] = BaseFieldDefinition::create('uuid') + ->setLabel(t('UUID')) + ->setDescription(t('The UUID of the Contact entity.')) + ->setReadOnly(TRUE); + + // Name field for the contact. + // We set display options for the view as well as the form. + // Users with correct privileges can change the view and edit configuration. + $fields['name'] = BaseFieldDefinition::create('string') + ->setLabel(t('Name')) + ->setDescription(t('The name of the Contact entity.')) + ->setSettings([ + 'max_length' => 255, + 'text_processing' => 0, + ]) + // Set no default value. + ->setDefaultValue(NULL) + ->setDisplayOptions('view', [ + 'label' => 'above', + 'type' => 'string', + 'weight' => -6, + ]) + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => -6, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['first_name'] = BaseFieldDefinition::create('string') + ->setLabel(t('First Name')) + ->setDescription(t('The first name of the Contact entity.')) + ->setSettings([ + 'max_length' => 255, + 'text_processing' => 0, + ]) + // Set no default value. + ->setDefaultValue(NULL) + ->setDisplayOptions('view', [ + 'label' => 'above', + 'type' => 'string', + 'weight' => -5, + ]) + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => -5, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + // Gender field for the contact. + // ListTextType with a drop down menu widget. + // The values shown in the menu are 'male' and 'female'. + // In the view the field content is shown as string. + // In the form the choices are presented as options list. + $fields['gender'] = BaseFieldDefinition::create('list_string') + ->setLabel(t('Gender')) + ->setDescription(t('The gender of the Contact entity.')) + ->setSettings([ + 'allowed_values' => [ + 'female' => 'female', + 'male' => 'male', + ], + ]) + ->setDisplayOptions('view', [ + 'label' => 'above', + 'type' => 'string', + 'weight' => -4, + ]) + ->setDisplayOptions('form', [ + 'type' => 'options_select', + 'weight' => -4, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + // Owner field of the contact. + // Entity reference field, holds the reference to the user object. + // The view shows the user name field of the user. + // The form presents a auto complete field for the user name. + $fields['user_id'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('User Name')) + ->setDescription(t('The Name of the associated user.')) + ->setSetting('target_type', 'user') + ->setSetting('handler', 'default') + ->setDisplayOptions('view', [ + 'label' => 'above', + 'type' => 'author', + 'weight' => -3, + ]) + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => 60, + 'placeholder' => '', + ], + 'weight' => -3, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + // Role field for the contact. + // The values shown in options are 'administrator' and 'user'. + $fields['role'] = BaseFieldDefinition::create('list_string') + ->setLabel(t('Role')) + ->setDescription(t('The role of the Contact entity.')) + ->setSettings([ + 'allowed_values' => [ + 'administrator' => 'administrator', + 'user' => 'user', + ], + ]) + // Set the default value of this field to 'user'. + ->setDefaultValue('user') + ->setDisplayOptions('view', [ + 'label' => 'above', + 'type' => 'string', + 'weight' => -2, + ]) + ->setDisplayOptions('form', [ + 'type' => 'options_select', + 'weight' => -2, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['langcode'] = BaseFieldDefinition::create('language') + ->setLabel(t('Language code')) + ->setDescription(t('The language code of ContentEntityExample entity.')); + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Created')) + ->setDescription(t('The time that the entity was created.')); + + $fields['changed'] = BaseFieldDefinition::create('changed') + ->setLabel(t('Changed')) + ->setDescription(t('The time that the entity was last edited.')); + + return $fields; + } + +} diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Entity/Controller/ContactListBuilder.php b/sites/all/modules/examples/examples/content_entity_example/src/Entity/Controller/ContactListBuilder.php new file mode 100644 index 000000000..1b271938d --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/src/Entity/Controller/ContactListBuilder.php @@ -0,0 +1,99 @@ +get('entity.manager')->getStorage($entity_type->id()), + $container->get('url_generator') + ); + } + + /** + * Constructs a new ContactListBuilder object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage class. + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator + * The url generator. + */ + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, UrlGeneratorInterface $url_generator) { + parent::__construct($entity_type, $storage); + $this->urlGenerator = $url_generator; + } + + /** + * {@inheritdoc} + * + * We override ::render() so that we can add our own content above the table. + * parent::render() is where EntityListBuilder creates the table using our + * buildHeader() and buildRow() implementations. + */ + public function render() { + $build['description'] = [ + '#markup' => $this->t('Content Entity Example implements a Contacts model. These contacts are fieldable entities. You can manage the fields on the Contacts admin page.', [ + '@adminlink' => $this->urlGenerator->generateFromRoute('content_entity_example.contact_settings'), + ]), + ]; + $build['table'] = parent::render(); + return $build; + } + + /** + * {@inheritdoc} + * + * Building the header and content lines for the contact list. + * + * Calling the parent::buildHeader() adds a column for the possible actions + * and inserts the 'edit' and 'delete' links as defined for the entity type. + */ + public function buildHeader() { + $header['id'] = $this->t('ContactID'); + $header['name'] = $this->t('Name'); + $header['first_name'] = $this->t('First Name'); + $header['gender'] = $this->t('Gender'); + $header['role'] = $this->t('Role'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /* @var $entity \Drupal\content_entity_example\Entity\Contact */ + $row['id'] = $entity->id(); + $row['name'] = $entity->link(); + $row['first_name'] = $entity->first_name->value; + $row['gender'] = $entity->gender->value; + $row['role'] = $entity->role->value; + return $row + parent::buildRow($entity); + } + +} diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactDeleteForm.php b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactDeleteForm.php new file mode 100644 index 000000000..c224dc21b --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactDeleteForm.php @@ -0,0 +1,56 @@ +t('Are you sure you want to delete entity %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + * + * If the delete command is canceled, return to the contact list. + */ + public function getCancelUrl() { + return new Url('entity.content_entity_example_contact.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + * + * Delete the entity and log the event. logger() replaces the watchdog. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $entity = $this->getEntity(); + $entity->delete(); + + $this->logger('content_entity_example')->notice('@type: deleted %title.', + [ + '@type' => $this->entity->bundle(), + '%title' => $this->entity->label(), + ]); + $form_state->setRedirect('entity.content_entity_example_contact.collection'); + } + +} diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactForm.php b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactForm.php new file mode 100644 index 000000000..e35a4624c --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactForm.php @@ -0,0 +1,42 @@ +entity; + + $form['langcode'] = [ + '#title' => $this->t('Language'), + '#type' => 'language_select', + '#default_value' => $entity->getUntranslated()->language()->getId(), + '#languages' => Language::STATE_ALL, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $form_state->setRedirect('entity.content_entity_example_contact.collection'); + $entity = $this->getEntity(); + $entity->save(); + } + +} diff --git a/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactSettingsForm.php b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactSettingsForm.php new file mode 100644 index 000000000..e44156c10 --- /dev/null +++ b/sites/all/modules/examples/examples/content_entity_example/src/Form/ContactSettingsForm.php @@ -0,0 +1,39 @@ +assertSession(); + + $web_user = $this->drupalCreateUser([ + 'add contact entity', + 'edit contact entity', + 'view contact entity', + 'delete contact entity', + 'administer contact entity', + 'administer content_entity_example_contact display', + 'administer content_entity_example_contact fields', + 'administer content_entity_example_contact form display', + ]); + + // Anonymous User should not see the link to the listing. + $assert->pageTextNotContains('Content Entity Example: Contacts Listing'); + + $this->drupalLogin($web_user); + + // Web_user user has the right to view listing. + $assert->linkExists('Content Entity Example: Contacts Listing'); + + $this->clickLink('Content Entity Example: Contacts Listing'); + + // WebUser can add entity content. + $assert->linkExists('Add Contact'); + + $this->clickLink(t('Add Contact')); + + $assert->fieldValueEquals('name[0][value]', ''); + $assert->fieldValueEquals('name[0][value]', ''); + $assert->fieldValueEquals('name[0][value]', ''); + $assert->fieldValueEquals('name[0][value]', ''); + + $user_ref = $web_user->name->value . ' (' . $web_user->id() . ')'; + $assert->fieldValueEquals('user_id[0][target_id]', $user_ref); + + // Post content, save an instance. Go back to list after saving. + $edit = [ + 'name[0][value]' => 'test name', + 'first_name[0][value]' => 'test first name', + 'gender' => 'male', + 'role' => 'administrator', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + + // Entity listed. + $assert->linkExists('Edit'); + $assert->linkExists('Delete'); + + $this->clickLink('test name'); + + // Entity shown. + $assert->pageTextContains('test name'); + $assert->pageTextContains('test first name'); + $assert->pageTextContains('administrator'); + $assert->pageTextContains('male'); + $assert->linkExists('Add Contact'); + $assert->linkExists('Edit'); + $assert->linkExists('Delete'); + + // Delete the entity. + $this->clickLink('Delete'); + + // Confirm deletion. + $assert->linkExists('Cancel'); + $this->drupalPostForm(NULL, [], 'Delete'); + + // Back to list, must be empty. + $assert->pageTextNotContains('test name'); + + // Settings page. + $this->drupalGet('admin/structure/content_entity_example_contact_settings'); + $assert->pageTextContains('Contact Settings'); + + // Make sure the field manipulation links are available. + $assert->linkExists('Settings'); + $assert->linkExists('Manage fields'); + $assert->linkExists('Manage form display'); + $assert->linkExists('Manage display'); + } + + /** + * Test all paths exposed by the module, by permission. + */ + public function testPaths() { + $assert = $this->assertSession(); + + // Generate a contact so that we can test the paths against it. + $contact = Contact::create( + [ + 'name' => 'somename', + 'first_name' => 'Joe', + 'gender' => 'female', + 'role' => 'administrator', + ] + ); + $contact->save(); + + // Gather the test data. + $data = $this->providerTestPaths($contact->id()); + + // Run the tests. + foreach ($data as $datum) { + // drupalCreateUser() doesn't know what to do with an empty permission + // array, so we help it out. + if ($datum[2]) { + $user = $this->drupalCreateUser([$datum[2]]); + $this->drupalLogin($user); + } + else { + $user = $this->drupalCreateUser(); + $this->drupalLogin($user); + } + $this->drupalGet($datum[1]); + $assert->statusCodeEquals($datum[0]); + } + } + + /** + * Data provider for testPaths. + * + * @param int $contact_id + * The id of an existing Contact entity. + * + * @return array + * Nested array of testing data. Arranged like this: + * - Expected response code. + * - Path to request. + * - Permission for the user. + */ + protected function providerTestPaths($contact_id) { + return [ + [ + 200, + '/content_entity_example_contact/' . $contact_id, + 'view contact entity', + ], + [ + 403, + '/content_entity_example_contact/' . $contact_id, + '', + ], + [ + 200, + '/content_entity_example_contact/list', + 'view contact entity', + ], + [ + 403, + '/content_entity_example_contact/list', + '', + ], + [ + 200, + '/content_entity_example_contact/add', + 'add contact entity', + ], + [ + 403, + '/content_entity_example_contact/add', + '', + ], + [ + 200, + '/content_entity_example_contact/' . $contact_id . '/edit', + 'edit contact entity', + ], + [ + 403, + '/content_entity_example_contact/' . $contact_id . '/edit', + '', + ], + [ + 200, + '/contact/' . $contact_id . '/delete', + 'delete contact entity', + ], + [ + 403, + '/contact/' . $contact_id . '/delete', + '', + ], + [ + 200, + 'admin/structure/content_entity_example_contact_settings', + 'administer contact entity', + ], + [ + 403, + 'admin/structure/content_entity_example_contact_settings', + '', + ], + ]; + } + + /** + * Test add new fields to the contact entity. + */ + public function testAddFields() { + $web_user = $this->drupalCreateUser([ + 'administer contact entity', + 'administer content_entity_example_contact display', + 'administer content_entity_example_contact fields', + 'administer content_entity_example_contact form display', + ]); + + $this->drupalLogin($web_user); + $entity_name = 'content_entity_example_contact'; + $add_field_url = 'admin/structure/' . $entity_name . '_settings/fields/add-field'; + $this->drupalGet($add_field_url); + $field_name = 'test_name'; + $edit = [ + 'new_storage_type' => 'list_string', + 'label' => 'test name', + 'field_name' => $field_name, + ]; + + $this->drupalPostForm(NULL, $edit, 'Save and continue'); + $expected_path = $this->buildUrl('admin/structure/' . $entity_name . '_settings/fields/' . $entity_name . '.' . $entity_name . '.field_' . $field_name . '/storage'); + + // Fetch url without query parameters. + $current_path = strtok($this->getUrl(), '?'); + $this->assertEquals($expected_path, $current_path); + } + +} diff --git a/sites/all/modules/examples/examples/cron_example/config/install/cron_example.settings.yml b/sites/all/modules/examples/examples/cron_example/config/install/cron_example.settings.yml new file mode 100644 index 000000000..8527484d1 --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/config/install/cron_example.settings.yml @@ -0,0 +1,4 @@ +# Default form values for \Drupal\cron_example\Form\CronExampleForm. +# @see examples/cron_example/config/schema/cron_example.schema.yml +# @see \Drupal\cron_example\Form\CronExampleForm::getEditableConfigNames() +interval: 300 diff --git a/sites/all/modules/examples/examples/cron_example/config/schema/cron_example.schema.yml b/sites/all/modules/examples/examples/cron_example/config/schema/cron_example.schema.yml new file mode 100644 index 000000000..f80c92cfc --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/config/schema/cron_example.schema.yml @@ -0,0 +1,12 @@ +# Set configuration defaults. This schema describes the cron_example.settings +# config. The defaults for this config are set in +# config/install/cron_example.settings.yml and then used by +# \Drupal\cron_example\Form\CronExampleForm. +# @see https://www.drupal.org/node/1905070 +cron_example.settings: + type: config_object + label: 'Cron Example settings' + mapping: + interval: + type: integer + label: 'Period between cron runs' diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.info.yml b/sites/all/modules/examples/examples/cron_example/cron_example.info.yml new file mode 100644 index 000000000..812ad2b29 --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/cron_example.info.yml @@ -0,0 +1,14 @@ +name: Cron Example +type: module +description: 'Demonstrates hook_cron() and related features' +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.links.menu.yml b/sites/all/modules/examples/examples/cron_example/cron_example.links.menu.yml new file mode 100644 index 000000000..faf80eb2d --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/cron_example.links.menu.yml @@ -0,0 +1,3 @@ +cron_example.description: + title: Cron Example + route_name: cron_example diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.module b/sites/all/modules/examples/examples/cron_example/cron_example.module new file mode 100644 index 000000000..217c280cd --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/cron_example.module @@ -0,0 +1,56 @@ +getEditable('cron_example.settings'); + // Default to an hourly interval. Of course, cron has to be running at least + // hourly for this to work. + $interval = $cron_config->get('interval'); + $interval = !empty($interval) ? $interval : 3600; + + // We usually don't want to act every time cron runs (which could be every + // minute) so keep a time for the next run in the site state. + $next_execution = \Drupal::state()->get('cron_example.next_execution'); + $next_execution = !empty($next_execution) ? $next_execution : 0; + if (REQUEST_TIME >= $next_execution) { + // This is a silly example of a cron job. + // It just makes it obvious that the job has run without + // making any changes to your database. + \Drupal::logger('cron_example')->notice('cron_example ran'); + if (\Drupal::state()->get('cron_example_show_status_message')) { + drupal_set_message(t('cron_example executed at %time', ['%time' => date_iso8601(REQUEST_TIME)])); + \Drupal::state()->set('cron_example_show_status_message', FALSE); + } + \Drupal::state()->set('cron_example.next_execution', REQUEST_TIME + $interval); + } +} + +/** + * @} End of "defgroup cron_example". + */ diff --git a/sites/all/modules/examples/examples/cron_example/cron_example.routing.yml b/sites/all/modules/examples/examples/cron_example/cron_example.routing.yml new file mode 100644 index 000000000..81afcf9ff --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/cron_example.routing.yml @@ -0,0 +1,7 @@ +cron_example: + path: '/examples/cron-example' + defaults: + _form: '\Drupal\cron_example\Form\CronExampleForm' + _title: 'Cron Example' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/cron_example/src/Form/CronExampleForm.php b/sites/all/modules/examples/examples/cron_example/src/Form/CronExampleForm.php new file mode 100644 index 000000000..8b70027cf --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/src/Form/CronExampleForm.php @@ -0,0 +1,256 @@ +currentUser = $current_user; + $this->cron = $cron; + $this->queue = $queue; + $this->state = $state; + + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('current_user'), + $container->get('cron'), + $container->get('queue'), + $container->get('state') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'cron_example'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->configFactory->get('cron_example.settings'); + + $form['status'] = [ + '#type' => 'details', + '#title' => $this->t('Cron status information'), + '#open' => TRUE, + ]; + $form['status']['intro'] = [ + '#type' => 'item', + '#markup' => $this->t('The cron example demonstrates hook_cron() and hook_queue_info() processing. If you have administrative privileges you can run cron from this page and see the results.'), + ]; + + $next_execution = \Drupal::state()->get('cron_example.next_execution'); + $next_execution = !empty($next_execution) ? $next_execution : REQUEST_TIME; + + $args = [ + '%time' => date_iso8601(\Drupal::state()->get('cron_example.next_execution')), + '%seconds' => $next_execution - REQUEST_TIME, + ]; + $form['status']['last'] = [ + '#type' => 'item', + '#markup' => $this->t('cron_example_cron() will next execute the first time cron runs after %time (%seconds seconds from now)', $args), + ]; + + if ($this->currentUser->hasPermission('administer site configuration')) { + $form['cron_run'] = [ + '#type' => 'details', + '#title' => $this->t('Run cron manually'), + '#open' => TRUE, + ]; + $form['cron_run']['cron_reset'] = [ + '#type' => 'checkbox', + '#title' => $this->t("Run cron_example's cron regardless of whether interval has expired."), + '#default_value' => FALSE, + ]; + $form['cron_run']['cron_trigger']['actions'] = ['#type' => 'actions']; + $form['cron_run']['cron_trigger']['actions']['sumbit'] = [ + '#type' => 'submit', + '#value' => $this->t('Run cron now'), + '#submit' => [[$this, 'cronRun']], + ]; + } + + $form['cron_queue_setup'] = [ + '#type' => 'details', + '#title' => $this->t('Cron queue setup (for hook_cron_queue_info(), etc.)'), + '#open' => TRUE, + ]; + + $queue_1 = $this->queue->get('cron_example_queue_1'); + $queue_2 = $this->queue->get('cron_example_queue_2'); + + $args = [ + '%queue_1' => $queue_1->numberOfItems(), + '%queue_2' => $queue_2->numberOfItems(), + ]; + $form['cron_queue_setup']['current_cron_queue_status'] = [ + '#type' => 'item', + '#markup' => $this->t('There are currently %queue_1 items in queue 1 and %queue_2 items in queue 2', $args), + ]; + $form['cron_queue_setup']['num_items'] = [ + '#type' => 'select', + '#title' => $this->t('Number of items to add to queue'), + '#options' => array_combine([1, 5, 10, 100, 1000], [1, 5, 10, 100, 1000]), + '#default_value' => 5, + ]; + $form['cron_queue_setup']['queue'] = [ + '#type' => 'radios', + '#title' => $this->t('Queue to add items to'), + '#options' => [ + 'cron_example_queue_1' => $this->t('Queue 1'), + 'cron_example_queue_2' => $this->t('Queue 2'), + ], + '#default_value' => 'cron_example_queue_1', + ]; + $form['cron_queue_setup']['actions'] = ['#type' => 'actions']; + $form['cron_queue_setup']['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Add jobs to queue'), + '#submit' => [[$this, 'addItems']], + ]; + + $form['configuration'] = [ + '#type' => 'details', + '#title' => $this->t('Configuration of cron_example_cron()'), + '#open' => TRUE, + ]; + $form['configuration']['cron_example_interval'] = [ + '#type' => 'select', + '#title' => $this->t('Cron interval'), + '#description' => $this->t('Time after which cron_example_cron will respond to a processing request.'), + '#default_value' => $config->get('interval'), + '#options' => [ + 60 => $this->t('1 minute'), + 300 => $this->t('5 minutes'), + 3600 => $this->t('1 hour'), + 86400 => $this->t('1 day'), + ], + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * Allow user to directly execute cron, optionally forcing it. + */ + public function cronRun(array &$form, FormStateInterface &$form_state) { + $config = $this->configFactory->getEditable('cron_example.settings'); + + $cron_reset = $form_state->getValue('cron_reset'); + if (!empty($cron_reset)) { + \Drupal::state()->set('cron_example.next_execution', 0); + } + + // Use a state variable to signal that cron was run manually from this form. + $this->state->set('cron_example_show_status_message', TRUE); + if ($this->cron->run()) { + drupal_set_message($this->t('Cron ran successfully.')); + } + else { + drupal_set_message($this->t('Cron run failed.'), 'error'); + } + } + + /** + * Add the items to the queue when signaled by the form. + */ + public function addItems(array &$form, FormStateInterface &$form_state) { + $values = $form_state->getValues(); + $queue_name = $form['cron_queue_setup']['queue'][$values['queue']]['#title']; + $num_items = $form_state->getValue('num_items'); + // Queues are defined by a QueueWorker Plugin which are selected by their + // id attritbute. + // @see \Drupal\cron_example\Plugin\QueueWorker\ReportWorkerOne + $queue = $this->queue->get($values['queue']); + + for ($i = 1; $i <= $num_items; $i++) { + // Create a new item, a new data object, which is passed to the + // QueueWorker's processItem() method. + $item = new \stdClass(); + $item->created = REQUEST_TIME; + $item->sequence = $i; + $queue->createItem($item); + } + + $args = [ + '%num' => $num_items, + '%queue' => $queue_name, + ]; + drupal_set_message($this->t('Added %num items to %queue', $args)); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Update the interval as stored in configuration. This will be read when + // this modules hook_cron function fires and will be used to ensure that + // action is taken only after the appropiate time has elapsed. + $this->configFactory->getEditable('cron_example.settings') + ->set('interval', $form_state->getValue('cron_example_interval')) + ->save(); + + parent::submitForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['cron_example.settings']; + } + +} diff --git a/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerBase.php b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerBase.php new file mode 100644 index 000000000..7377b7dce --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerBase.php @@ -0,0 +1,92 @@ +state = $state; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('state'), + $container->get('logger.factory') + ); + } + + /** + * Simple reporter log and display information about the queue. + * + * @param int $worker + * Worker number. + * @param object $item + * The $item which was stored in the cron queue. + */ + protected function reportWork($worker, $item) { + if ($this->state->get('cron_example_show_status_message')) { + drupal_set_message( + $this->t('Queue @worker worker processed item with sequence @sequence created at @time', [ + '@worker' => $worker, + '@sequence' => $item->sequence, + '@time' => date_iso8601($item->created), + ]) + ); + } + $this->logger->get('cron_example')->info('Queue @worker worker processed item with sequence @sequence created at @time', [ + '@worker' => $worker, + '@sequence' => $item->sequence, + '@time' => date_iso8601($item->created), + ]); + } + +} diff --git a/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerOne.php b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerOne.php new file mode 100644 index 000000000..653223bd7 --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerOne.php @@ -0,0 +1,29 @@ +reportWork(1, $data); + } + +} diff --git a/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerTwo.php b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerTwo.php new file mode 100644 index 000000000..b4fad9a2f --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/src/Plugin/QueueWorker/ReportWorkerTwo.php @@ -0,0 +1,29 @@ +reportWork(2, $data); + } + +} diff --git a/sites/all/modules/examples/examples/cron_example/tests/src/Functional/CronExampleTest.php b/sites/all/modules/examples/examples/cron_example/tests/src/Functional/CronExampleTest.php new file mode 100644 index 000000000..c3b07164c --- /dev/null +++ b/sites/all/modules/examples/examples/cron_example/tests/src/Functional/CronExampleTest.php @@ -0,0 +1,90 @@ +drupalLogin($this->drupalCreateUser(['administer site configuration', 'access content'])); + + $this->cronConfig = \Drupal::configFactory()->getEditable('cron_example.settings'); + } + + /** + * Create an example node, test block through admin and user interfaces. + */ + public function testCronExampleBasic() { + $assert = $this->assertSession(); + + // Pretend that cron has never been run (even though simpletest seems to + // run it once...). + \Drupal::state()->set('cron_example.next_execution', 0); + $this->drupalGet('examples/cron-example'); + + // Initial run should cause cron_example_cron() to fire. + $post = []; + $this->drupalPostForm('examples/cron-example', $post, 'Run cron now'); + $assert->pageTextContains('cron_example executed at'); + + // Forcing should also cause cron_example_cron() to fire. + $post['cron_reset'] = TRUE; + $this->drupalPostForm(NULL, $post, 'Run cron now'); + $assert->pageTextContains('cron_example executed at'); + + // But if followed immediately and not forced, it should not fire. + $post['cron_reset'] = FALSE; + $this->drupalPostForm(NULL, $post, 'Run cron now'); + $assert->statusCodeEquals(200); + $assert->pageTextNotContains('cron_example executed at'); + $assert->pageTextContains('There are currently 0 items in queue 1 and 0 items in queue 2'); + + $post = [ + 'num_items' => 5, + 'queue' => 'cron_example_queue_1', + ]; + $this->drupalPostForm(NULL, $post, 'Add jobs to queue'); + $assert->pageTextContains('There are currently 5 items in queue 1 and 0 items in queue 2'); + + $post = [ + 'num_items' => 100, + 'queue' => 'cron_example_queue_2', + ]; + $this->drupalPostForm(NULL, $post, 'Add jobs to queue'); + $assert->pageTextContains('There are currently 5 items in queue 1 and 100 items in queue 2'); + + $this->drupalPostForm('examples/cron-example', [], 'Run cron now'); + $assert->responseMatches('/Queue 1 worker processed item with sequence 5 /'); + $assert->responseMatches('/Queue 2 worker processed item with sequence 100 /'); + } + +} diff --git a/sites/all/modules/examples/examples/css/examples.icons.css b/sites/all/modules/examples/examples/css/examples.icons.css new file mode 100644 index 000000000..da9645465 --- /dev/null +++ b/sites/all/modules/examples/examples/css/examples.icons.css @@ -0,0 +1,13 @@ +/** + * @file + * Styling for the user module icons. + */ + +/** + * Toolbar tab icon. + */ +.toolbar-bar .toolbar-icon-examples:before, +.toolbar-bar .toolbar-icon-examples:active:before, +.toolbar-bar .toolbar-icon-examples.active:before { + background-image: url(../images/examples.svg); +} diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.info.yml b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.info.yml new file mode 100644 index 000000000..57858f94d --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.info.yml @@ -0,0 +1,15 @@ +name: DBTNG Example +type: module +description: 'Demonstrates how to use the database API: DBTNG.' +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - drupal:user + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.install b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.install new file mode 100644 index 000000000..a1c729814 --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.install @@ -0,0 +1,95 @@ + 'John', + 'surname' => 'Doe', + 'age' => 0, + ]; + db_insert('dbtng_example') + ->fields($fields) + ->execute(); + + // Add another entry. + $fields = [ + 'name' => 'John', + 'surname' => 'Roe', + 'age' => 100, + 'uid' => 1, + ]; + db_insert('dbtng_example') + ->fields($fields) + ->execute(); +} + +/** + * Implements hook_schema(). + * + * Defines the database tables used by this module. + * + * @see hook_schema() + * + * @ingroup dbtng_example + */ +function dbtng_example_schema() { + $schema['dbtng_example'] = [ + 'description' => 'Stores example person entries for demonstration purposes.', + 'fields' => [ + 'pid' => [ + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique person ID.', + ], + 'uid' => [ + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => "Creator user's {users}.uid", + ], + 'name' => [ + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Name of the person.', + ], + 'surname' => [ + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Surname of the person.', + ], + 'age' => [ + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => 'The age of the person in years.', + ], + ], + 'primary key' => ['pid'], + 'indexes' => [ + 'name' => ['name'], + 'surname' => ['surname'], + 'age' => ['age'], + ], + ]; + + return $schema; +} diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.links.menu.yml b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.links.menu.yml new file mode 100644 index 000000000..92270d8c2 --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.links.menu.yml @@ -0,0 +1,25 @@ +dbtng_example.description: + title: DBTNG Example + route_name: dbtng_example + expanded: TRUE + +dbtng_example.list: + title: List + route_name: dbtng_list + weight: 0 + parent: dbtng_example.description +dbtng_example.add: + title: Add entry + route_name: dbtng_add + weight: 1 + parent: dbtng_example.description +dbtng_example.update: + title: Update entry + route_name: dbtng_update + weight: 2 + parent: dbtng_example.description +dbtng_example.advanced: + title: Advanced list + route_name: dbtng_advanced + weight: 3 + parent: dbtng_example.description diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.module b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.module new file mode 100644 index 000000000..070ee5b5f --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.module @@ -0,0 +1,63 @@ +fields(array('name' => 'John', 'surname' => 'Doe')) + * ->execute(); + * @endcode + * + * db_update() example: + * @code + * // UPDATE {dbtng_example} SET name = 'Jane' WHERE name = 'John' + * db_update('dbtng_example') + * ->fields(array('name' => 'Jane')) + * ->condition('name', 'John') + * ->execute(); + * @endcode + * + * db_delete() example: + * @code + * // DELETE FROM {dbtng_example} WHERE name = 'Jane' + * db_delete('dbtng_example') + * ->condition('name', 'Jane') + * ->execute(); + * @endcode + * + * See @link database Database Abstraction Layer @endlink + * @see db_insert() + * @see db_update() + * @see db_delete() + */ + +/** + * @} End of "defgroup dbtng_example". + */ diff --git a/sites/all/modules/examples/examples/dbtng_example/dbtng_example.routing.yml b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.routing.yml new file mode 100644 index 000000000..d301a8138 --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/dbtng_example.routing.yml @@ -0,0 +1,39 @@ +dbtng_example: + path: 'examples/dbtng-example' + defaults: + _title: 'DBTNG Example' + _controller: '\Drupal\dbtng_example\Controller\DbtngExampleController::entryList' + requirements: + _permission: 'access content' + +dbtng_list: + path: 'examples/dbtng-example/list' + defaults: + _title: 'List' + _controller: '\Drupal\dbtng_example\Controller\DbtngExampleController::entryList' + requirements: + _permission: 'access content' + +dbtng_add: + path: 'examples/dbtng-example/add' + defaults: + _title: 'Add entry' + _form: '\Drupal\dbtng_example\Form\DbtngExampleAddForm' + requirements: + _permission: 'access content' + +dbtng_update: + path: 'examples/dbtng-example/update' + defaults: + _title: 'Update entry' + _form: '\Drupal\dbtng_example\Form\DbtngExampleUpdateForm' + requirements: + _permission: 'access content' + +dbtng_advanced: + path: 'examples/dbtng-example/advanced' + defaults: + _title: 'Advanced list' + _controller: '\Drupal\dbtng_example\Controller\DbtngExampleController::entryAdvancedList' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/dbtng_example/src/Controller/DbtngExampleController.php b/sites/all/modules/examples/examples/dbtng_example/src/Controller/DbtngExampleController.php new file mode 100644 index 000000000..04f026b6e --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/src/Controller/DbtngExampleController.php @@ -0,0 +1,78 @@ + $this->t('Generate a list of all entries in the database. There is no filter in the query.'), + ]; + + $rows = []; + $headers = [t('Id'), t('uid'), t('Name'), t('Surname'), t('Age')]; + + foreach ($entries = DbtngExampleStorage::load() as $entry) { + // Sanitize each entry. + $rows[] = array_map('Drupal\Component\Utility\SafeMarkup::checkPlain', (array) $entry); + } + $content['table'] = [ + '#type' => 'table', + '#header' => $headers, + '#rows' => $rows, + '#empty' => t('No entries available.'), + ]; + // Don't cache this page. + $content['#cache']['max-age'] = 0; + + return $content; + } + + /** + * Render a filtered list of entries in the database. + */ + public function entryAdvancedList() { + $content = []; + + $content['message'] = [ + '#markup' => $this->t('A more complex list of entries in the database.') . ' ' . + $this->t('Only the entries with name = "John" and age older than 18 years are shown, the username of the person who created the entry is also shown.'), + ]; + + $headers = [ + t('Id'), + t('Created by'), + t('Name'), + t('Surname'), + t('Age'), + ]; + + $rows = []; + foreach ($entries = DbtngExampleStorage::advancedLoad() as $entry) { + // Sanitize each entry. + $rows[] = array_map('Drupal\Component\Utility\SafeMarkup::checkPlain', $entry); + } + $content['table'] = [ + '#type' => 'table', + '#header' => $headers, + '#rows' => $rows, + '#attributes' => ['id' => 'dbtng-example-advanced-list'], + '#empty' => t('No entries available.'), + ]; + // Don't cache this page. + $content['#cache']['max-age'] = 0; + return $content; + } + +} diff --git a/sites/all/modules/examples/examples/dbtng_example/src/DbtngExampleStorage.php b/sites/all/modules/examples/examples/dbtng_example/src/DbtngExampleStorage.php new file mode 100644 index 000000000..eea0a69d6 --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/src/DbtngExampleStorage.php @@ -0,0 +1,222 @@ +fields($entry) + ->execute(); + } + catch (\Exception $e) { + drupal_set_message(t('db_insert failed. Message = %message, query= %query', [ + '%message' => $e->getMessage(), + '%query' => $e->query_string, + ] + ), 'error'); + } + return $return_value; + } + + /** + * Update an entry in the database. + * + * @param array $entry + * An array containing all the fields of the item to be updated. + * + * @return int + * The number of updated rows. + * + * @see db_update() + */ + public static function update(array $entry) { + try { + // db_update()...->execute() returns the number of rows updated. + $count = db_update('dbtng_example') + ->fields($entry) + ->condition('pid', $entry['pid']) + ->execute(); + } + catch (\Exception $e) { + drupal_set_message(t('db_update failed. Message = %message, query= %query', [ + '%message' => $e->getMessage(), + '%query' => $e->query_string, + ] + ), 'error'); + } + return $count; + } + + /** + * Delete an entry from the database. + * + * @param array $entry + * An array containing at least the person identifier 'pid' element of the + * entry to delete. + * + * @see db_delete() + */ + public static function delete(array $entry) { + db_delete('dbtng_example') + ->condition('pid', $entry['pid']) + ->execute(); + } + + /** + * Read from the database using a filter array. + * + * The standard function to perform reads was db_query(), and for static + * queries, it still is. + * + * db_query() used an SQL query with placeholders and arguments as parameters. + * + * Drupal DBTNG provides an abstracted interface that will work with a wide + * variety of database engines. + * + * db_query() is deprecated except when doing a static query. The following is + * perfectly acceptable in Drupal 8. See + * @link http://drupal.org/node/310072 the handbook page on static queries @endlink + * + * @code + * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John' + * db_query( + * "SELECT * FROM {dbtng_example} WHERE uid = :uid and name = :name", + * array(':uid' => 0, ':name' => 'John') + * )->execute(); + * @endcode + * + * But for more dynamic queries, Drupal provides the db_select() + * API method, so there are several ways to perform the same SQL query. See + * the + * @link http://drupal.org/node/310075 handbook page on dynamic queries. @endlink + * @code + * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John' + * db_select('dbtng_example') + * ->fields('dbtng_example') + * ->condition('uid', 0) + * ->condition('name', 'John') + * ->execute(); + * @endcode + * + * Here is db_select with named placeholders: + * @code + * // SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John' + * $arguments = array(':name' => 'John', ':uid' => 0); + * db_select('dbtng_example') + * ->fields('dbtng_example') + * ->where('uid = :uid AND name = :name', $arguments) + * ->execute(); + * @endcode + * + * Conditions are stacked and evaluated as AND and OR depending on the type of + * query. For more information, read the conditional queries handbook page at: + * http://drupal.org/node/310086 + * + * The condition argument is an 'equal' evaluation by default, but this can be + * altered: + * @code + * // SELECT * FROM {dbtng_example} WHERE age > 18 + * db_select('dbtng_example') + * ->fields('dbtng_example') + * ->condition('age', 18, '>') + * ->execute(); + * @endcode + * + * @param array $entry + * An array containing all the fields used to search the entries in the + * table. + * + * @return object + * An object containing the loaded entries if found. + * + * @see db_select() + * @see db_query() + * @see http://drupal.org/node/310072 + * @see http://drupal.org/node/310075 + */ + public static function load(array $entry = []) { + // Read all fields from the dbtng_example table. + $select = db_select('dbtng_example', 'example'); + $select->fields('example'); + + // Add each field and value as a condition to this query. + foreach ($entry as $field => $value) { + $select->condition($field, $value); + } + // Return the result in object format. + return $select->execute()->fetchAll(); + } + + /** + * Load dbtng_example records joined with user records. + * + * DBTNG also helps processing queries that return several rows, providing the + * found objects in the same query execution call. + * + * This function queries the database using a JOIN between users table and the + * example entries, to provide the username that created the entry, and + * creates a table with the results, processing each row. + * + * SELECT + * e.pid as pid, e.name as name, e.surname as surname, e.age as age + * u.name as username + * FROM + * {dbtng_example} e + * JOIN + * users u ON e.uid = u.uid + * WHERE + * e.name = 'John' AND e.age > 18 + * + * @see db_select() + * @see http://drupal.org/node/310075 + */ + public static function advancedLoad() { + $select = db_select('dbtng_example', 'e'); + // Join the users table, so we can get the entry creator's username. + $select->join('users_field_data', 'u', 'e.uid = u.uid'); + // Select these specific fields for the output. + $select->addField('e', 'pid'); + $select->addField('u', 'name', 'username'); + $select->addField('e', 'name'); + $select->addField('e', 'surname'); + $select->addField('e', 'age'); + // Filter only persons named "John". + $select->condition('e.name', 'John'); + // Filter only persons older than 18 years. + $select->condition('e.age', 18, '>'); + // Make sure we only get items 0-49, for scalability reasons. + $select->range(0, 50); + + $entries = $select->execute()->fetchAll(\PDO::FETCH_ASSOC); + + return $entries; + } + +} diff --git a/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleAddForm.php b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleAddForm.php new file mode 100644 index 000000000..04f5a839d --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleAddForm.php @@ -0,0 +1,130 @@ +get('current_user') + ); + // The StringTranslationTrait trait manages the string translation service + // for us. We can inject the service here. + $form->setStringTranslation($container->get('string_translation')); + return $form; + } + + /** + * Construct the new form object. + */ + public function __construct(AccountProxyInterface $current_user) { + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'dbtng_add_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = []; + + $form['message'] = [ + '#markup' => $this->t('Add an entry to the dbtng_example table.'), + ]; + + $form['add'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Add a person entry'), + ]; + $form['add']['name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Name'), + '#size' => 15, + ]; + $form['add']['surname'] = [ + '#type' => 'textfield', + '#title' => $this->t('Surname'), + '#size' => 15, + ]; + $form['add']['age'] = [ + '#type' => 'textfield', + '#title' => $this->t('Age'), + '#size' => 5, + '#description' => $this->t("Values greater than 127 will cause an exception. Try it - it's a great example why exception handling is needed with DTBNG."), + ]; + $form['add']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Add'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // Verify that the user is logged-in. + if ($this->currentUser->isAnonymous()) { + $form_state->setError($form['add'], $this->t('You must be logged in to add values to the database.')); + } + // Confirm that age is numeric. + if (!intval($form_state->getValue('age'))) { + $form_state->setErrorByName('age', $this->t('Age needs to be a number')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Gather the current user so the new record has ownership. + $account = $this->currentUser; + // Save the submitted entry. + $entry = [ + 'name' => $form_state->getValue('name'), + 'surname' => $form_state->getValue('surname'), + 'age' => $form_state->getValue('age'), + 'uid' => $account->id(), + ]; + $return = DbtngExampleStorage::insert($entry); + if ($return) { + drupal_set_message($this->t('Created entry @entry', ['@entry' => print_r($entry, TRUE)])); + } + } + +} diff --git a/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleUpdateForm.php b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleUpdateForm.php new file mode 100644 index 000000000..8226bd95f --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/src/Form/DbtngExampleUpdateForm.php @@ -0,0 +1,153 @@ + '
', + '#suffix' => '
', + ]; + // Add some explanatory text to the form. + $form['message'] = [ + '#markup' => $this->t('Demonstrates a database update operation.'), + ]; + // Query for items to display. + $entries = DbtngExampleStorage::load(); + // Tell the user if there is nothing to display. + if (empty($entries)) { + $form['no_values'] = [ + '#value' => t('No entries exist in the table dbtng_example table.'), + ]; + return $form; + } + + $keyed_entries = []; + foreach ($entries as $entry) { + $options[$entry->pid] = t('@pid: @name @surname (@age)', [ + '@pid' => $entry->pid, + '@name' => $entry->name, + '@surname' => $entry->surname, + '@age' => $entry->age, + ]); + $keyed_entries[$entry->pid] = $entry; + } + + // Grab the pid. + $pid = $form_state->getValue('pid'); + // Use the pid to set the default entry for updating. + $default_entry = !empty($pid) ? $keyed_entries[$pid] : $entries[0]; + + // Save the entries into the $form_state. We do this so the AJAX callback + // doesn't need to repeat the query. + $form_state->setValue('entries', $keyed_entries); + + $form['pid'] = [ + '#type' => 'select', + '#options' => $options, + '#title' => t('Choose entry to update'), + '#default_value' => $default_entry->pid, + '#ajax' => [ + 'wrapper' => 'updateform', + 'callback' => [$this, 'updateCallback'], + ], + ]; + + $form['name'] = [ + '#type' => 'textfield', + '#title' => t('Updated first name'), + '#size' => 15, + '#default_value' => $default_entry->name, + ]; + + $form['surname'] = [ + '#type' => 'textfield', + '#title' => t('Updated last name'), + '#size' => 15, + '#default_value' => $default_entry->surname, + ]; + $form['age'] = [ + '#type' => 'textfield', + '#title' => t('Updated age'), + '#size' => 4, + '#default_value' => $default_entry->age, + '#description' => t('Values greater than 127 will cause an exception'), + ]; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => t('Update'), + ]; + return $form; + } + + /** + * AJAX callback handler for the pid select. + * + * When the pid changes, populates the defaults from the database in the form. + */ + public function updateCallback(array $form, FormStateInterface $form_state) { + // Gather the DB results from $form_state. + $entries = $form_state->getValue('entries'); + // Use the specific entry for this $form_state. + $entry = $entries[$form_state->getValue('pid')]; + // Setting the #value of items is the only way I was able to figure out + // to get replaced defaults on these items. #default_value will not do it + // and shouldn't. + foreach (['name', 'surname', 'age'] as $item) { + $form[$item]['#value'] = $entry->$item; + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // Confirm that age is numeric. + if (!intval($form_state->getValue('age'))) { + $form_state->setErrorByName('age', t('Age needs to be a number')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Gather the current user so the new record has ownership. + $account = $this->currentUser(); + // Save the submitted entry. + $entry = [ + 'pid' => $form_state->getValue('pid'), + 'name' => $form_state->getValue('name'), + 'surname' => $form_state->getValue('surname'), + 'age' => $form_state->getValue('age'), + 'uid' => $account->id(), + ]; + $count = DbtngExampleStorage::update($entry); + drupal_set_message(t('Updated entry @entry (@count row updated)', [ + '@count' => $count, + '@entry' => print_r($entry, TRUE), + ])); + } + +} diff --git a/sites/all/modules/examples/examples/dbtng_example/tests/src/Functional/DbtngExampleTest.php b/sites/all/modules/examples/examples/dbtng_example/tests/src/Functional/DbtngExampleTest.php new file mode 100644 index 000000000..5fd591a2b --- /dev/null +++ b/sites/all/modules/examples/examples/dbtng_example/tests/src/Functional/DbtngExampleTest.php @@ -0,0 +1,222 @@ +assertSession(); + + // Assert that two entries were inserted at install. + $result = DbtngExampleStorage::load(); + $this->assertCount(2, $result, 'Did not find two entries in the table after installing the module.'); + + // Test the example description page. + $this->drupalGet('/examples/dbtng-example'); + $assert->statusCodeEquals(200); + + // Verify and validate that default menu links were loaded for this module. + $links = $this->providerMenuLinks(); + foreach ($links as $page => $hrefs) { + foreach ($hrefs as $href) { + $this->drupalGet($page); + $assert->linkByHrefExists($href); + } + } + } + + /** + * Data provider for testing menu links. + * + * @return array + * Array of page -> link relationships to check for: + * - The key is the path to the page where our link should appear. + * - The value is an array of links that should appear on that page. + */ + protected function providerMenuLinks() { + return [ + '' => [ + '/examples/dbtng-example', + ], + '/examples/dbtng-example' => [ + '/examples/dbtng-example/add', + '/examples/dbtng-example/update', + '/examples/dbtng-example/advanced', + ], + ]; + } + + /** + * Test the UI. + */ + public function testUi() { + $assert = $this->assertSession(); + + $this->drupalLogin($this->createUser()); + // Test the basic list. + $this->drupalGet('/examples/dbtng-example'); + $assert->statusCodeEquals(200); + $assert->pageTextMatches('%John[td/<>\w\s]+Doe%'); + + // Test the add tab. + // Add the new entry. + $this->drupalPostForm( + '/examples/dbtng-example/add', + [ + 'name' => 'Some', + 'surname' => 'Anonymous', + 'age' => 33, + ], + 'Add' + ); + // Now find the new entry. + $this->drupalGet('/examples/dbtng-example'); + $assert->pageTextMatches('%Some[td/<>\w\s]+Anonymous%'); + // Try the update tab. + // Find out the pid of our "anonymous" guy. + $result = DbtngExampleStorage::load(['surname' => 'Anonymous']); + $this->drupalGet('/examples/dbtng-example'); + $this->assertCount(1, $result, 'Did not find one entry in the table with surname = "Anonymous".'); + $entry = $result[0]; + unset($entry->uid); + + $entry = ['name' => 'NewFirstName', 'age' => 22]; + $this->drupalPostForm('/examples/dbtng-example/update', $entry, 'Update'); + // Now find the new entry. + $this->drupalGet('/examples/dbtng-example'); + $assert->pageTextMatches('%NewFirstName[td/<>\w\s]+Anonymous%'); + + // Try the advanced tab. + $this->drupalGet('/examples/dbtng-example/advanced'); + $rows = $this->xpath("//*[@id='dbtng-example-advanced-list'][1]/tbody/tr"); + $this->assertCount(1, $rows); + + $field = $this->xpath("//*[@id='dbtng-example-advanced-list'][1]/tbody/tr/td[4]"); + $this->assertEquals('Roe', $field[0]->getText()); + + // Try to add an entry while logged out. + $this->drupalLogout(); + $this->drupalPostForm( + '/examples/dbtng-example/add', + [ + 'name' => 'Anonymous', + 'surname' => 'UserCannotPost', + 'age' => 'not a number', + ], + 'Add' + ); + $assert->pageTextContains('You must be logged in to add values to the database.'); + $assert->pageTextContains('Age needs to be a number'); + } + + /** + * Tests several combinations, adding entries, updating and deleting. + */ + public function testDbtngExampleStorage() { + // Create a new entry. + $entry = [ + 'name' => 'James', + 'surname' => 'Doe', + 'age' => 23, + ]; + DbtngExampleStorage::insert($entry); + + // Save another entry. + $entry = [ + 'name' => 'Jane', + 'surname' => 'NotDoe', + 'age' => 19, + ]; + DbtngExampleStorage::insert($entry); + + // Verify that 4 records are found in the database. + $result = DbtngExampleStorage::load(); + $this->assertCount(4, $result); + + // Verify 2 of these records have 'Doe' as surname. + $result = DbtngExampleStorage::load(['surname' => 'Doe']); + $this->assertCount(2, $result, 'Did not find two entries in the table with surname = "Doe".'); + + // Now find our not-Doe entry. + $result = DbtngExampleStorage::load(['surname' => 'NotDoe']); + // Found one entry in the table with surname "NotDoe'. + $this->assertCount(1, $result, 'Did not find one entry in the table with surname "NotDoe'); + // Our NotDoe will be changed to "NowDoe". + $entry = $result[0]; + $entry->surname = "NowDoe"; + // update() returns the number of entries updated. + $this->assertNotEquals(DbtngExampleStorage::update((array) $entry), 0); + + $result = DbtngExampleStorage::load(['surname' => 'NowDoe']); + $this->assertCount(1, $result, "Did not find renamed 'NowDoe' surname."); + + // Read only John Doe entry. + $result = DbtngExampleStorage::load(['name' => 'John', 'surname' => 'Doe']); + $this->assertCount(1, $result, 'Did not find one entry for John Doe.'); + + // Get the entry. + $entry = (array) end($result); + // Change age to 45. + $entry['age'] = 45; + // Update entry in database. + DbtngExampleStorage::update((array) $entry); + + // Find entries with age = 45. + // Read only John Doe entry. + $result = DbtngExampleStorage::load(['surname' => 'NowDoe']); + // Found one entry with surname = Nowdoe. + $this->assertCount(1, $result, 'Did not find one entry with surname = Nowdoe.'); + + // Verify it is Jane NowDoe. + $entry = (array) end($result); + // The name Jane is found in the entry. + $this->assertEquals('Jane', $entry['name'], 'The name Jane is not found in the entry.'); + // The surname NowDoe is found in the entry. + $this->assertEquals('NowDoe', $entry['surname'], 'The surname NowDoe is not found in the entry.'); + + // Delete the entry. + DbtngExampleStorage::delete($entry); + + // Verify that now there are only 3 records. + $result = DbtngExampleStorage::load(); + // Found only three records, a record was deleted. + $this->assertCount(3, $result, 'Did not find only three records, a record might not have been deleted.'); + } + +} diff --git a/sites/all/modules/examples/examples/email_example/email_example.info.yml b/sites/all/modules/examples/examples/email_example/email_example.info.yml new file mode 100644 index 000000000..2cacfd577 --- /dev/null +++ b/sites/all/modules/examples/examples/email_example/email_example.info.yml @@ -0,0 +1,14 @@ +name: Email Example +type: module +description: Demonstrates how to send and alter Drupal-generated email messages. +package: Example modules +# core: 8.x +dependencies: + - examples:examples + - drupal:node + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/email_example/email_example.links.menu.yml b/sites/all/modules/examples/examples/email_example/email_example.links.menu.yml new file mode 100644 index 000000000..50f95e021 --- /dev/null +++ b/sites/all/modules/examples/examples/email_example/email_example.links.menu.yml @@ -0,0 +1,4 @@ +email_example.description: + title: 'E-mail Example: Contact Form' + description: 'Callback for generating form.' + route_name: email_example.description diff --git a/sites/all/modules/examples/examples/email_example/email_example.module b/sites/all/modules/examples/examples/email_example/email_example.module new file mode 100644 index 000000000..94beb6cb9 --- /dev/null +++ b/sites/all/modules/examples/examples/email_example/email_example.module @@ -0,0 +1,99 @@ + $message['langcode'], + ]; + + switch ($key) { + // Send a simple message from the contact form. + case 'contact_message': + $from = \Drupal::config('system.site')->get('mail'); + $message['subject'] = t('E-mail sent from @site-name', ['@site-name' => $from], $options); + // Note that the message body is an array, not a string. + $account = \Drupal::currentUser(); + $message['body'][] = t('@name sent you the following message:', ['@name' => $account->getUsername()], $options); + // Because this is just user-entered text, we do not need to translate it. + // Since user-entered text may have unintentional HTML entities in it like + // '<' or '>', we need to make sure these entities are properly escaped, + // as the body will later be transformed from HTML to text, meaning + // that a normal use of '<' will result in truncation of the message. + $message['body'][] = SafeMarkup::checkPlain($params['message']); + break; + } +} + +/** + * Implements hook_mail_alter(). + * + * This function is not required to send an email using Drupal's mail system. + * + * hook_mail_alter() provides an interface to alter any aspect of email sent by + * Drupal. You can use this hook to add a common site footer to all outgoing + * email, add extra header fields, and/or modify the email in anyway. HTML-izing + * the outgoing email is one possibility. + */ +function email_example_mail_alter(&$message) { + // For the purpose of this example, modify all the outgoing messages and + // attach a site signature. The signature will be translated to the language + // in which message was built. + $options = [ + 'langcode' => $message['langcode'], + ]; + + $signature = t("\n--\nMail altered by email_example module.", [], $options); + if (is_array($message['body'])) { + $message['body'][] = $signature; + } + else { + // Some modules use the body as a string, erroneously. + $message['body'] .= $signature; + } +} + +/** + * @} End of "defgroup email_example". + */ diff --git a/sites/all/modules/examples/examples/email_example/email_example.routing.yml b/sites/all/modules/examples/examples/email_example/email_example.routing.yml new file mode 100644 index 000000000..cefa64432 --- /dev/null +++ b/sites/all/modules/examples/examples/email_example/email_example.routing.yml @@ -0,0 +1,6 @@ +email_example.description: + path: 'examples/email-example' + defaults: + _form: '\Drupal\email_example\Form\EmailExampleGetFormPage' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/email_example/src/Form/EmailExampleGetFormPage.php b/sites/all/modules/examples/examples/email_example/src/Form/EmailExampleGetFormPage.php new file mode 100644 index 000000000..8951e0560 --- /dev/null +++ b/sites/all/modules/examples/examples/email_example/src/Form/EmailExampleGetFormPage.php @@ -0,0 +1,148 @@ +mailManager = $mail_manager; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.mail'), + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'email_example'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['intro'] = [ + '#markup' => t('Use this form to send a message to an e-mail address. No spamming!'), + ]; + $form['email'] = [ + '#type' => 'textfield', + '#title' => t('E-mail address'), + '#required' => TRUE, + ]; + $form['message'] = [ + '#type' => 'textarea', + '#title' => t('Message'), + '#required' => TRUE, + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => t('Submit'), + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if (!valid_email_address($form_state->getValue('email'))) { + $form_state->setErrorByName('email', t('That e-mail address is not valid.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + + // All system mails need to specify the module and template key (mirrored + // from hook_mail()) that the message they want to send comes from. + $module = 'email_example'; + $key = 'contact_message'; + + // Specify 'to' and 'from' addresses. + $to = $form_values['email']; + $from = $this->config('system.site')->get('mail'); + + // "params" loads in additional context for email content completion in + // hook_mail(). In this case, we want to pass in the values the user entered + // into the form, which include the message body in $form_values['message']. + $params = $form_values; + + // The language of the e-mail. This will one of three values: + // - $account->getPreferredLangcode(): Used for sending mail to a particular + // website user, so that the mail appears in their preferred language. + // - \Drupal::currentUser()->getPreferredLangcode(): Used when sending a + // mail back to the user currently viewing the site. This will send it in + // the language they're currently using. + // - \Drupal::languageManager()->getDefaultLanguage()->getId: Used when + // sending mail to a pre-existing, 'neutral' address, such as the system + // e-mail address, or when you're unsure of the language preferences of + // the intended recipient. + // + // Since in our case, we are sending a message to a random e-mail address + // that is not necessarily tied to a user account, we will use the site's + // default language. + $language_code = $this->languageManager->getDefaultLanguage()->getId(); + + // Whether or not to automatically send the mail when we call mail() on the + // mail manager. This defaults to TRUE, and is normally what you want unless + // you need to do additional processing before the mail manager sends the + // message. + $send_now = TRUE; + // Send the mail, and check for success. Note that this does not guarantee + // message delivery; only that there were no PHP-related issues encountered + // while sending. + $result = $this->mailManager->mail($module, $key, $to, $language_code, $params, $from, $send_now); + if ($result['result'] == TRUE) { + drupal_set_message(t('Your message has been sent.')); + } + else { + drupal_set_message(t('There was a problem sending your message and it was not sent.'), 'error'); + } + } + +} diff --git a/sites/all/modules/examples/examples/email_example/tests/src/Functional/EmailExampleTest.php b/sites/all/modules/examples/examples/email_example/tests/src/Functional/EmailExampleTest.php new file mode 100644 index 000000000..2c7ab55d0 --- /dev/null +++ b/sites/all/modules/examples/examples/email_example/tests/src/Functional/EmailExampleTest.php @@ -0,0 +1,73 @@ +assertSession(); + // Test for a link to the email_example in the Tools menu. + $this->drupalGet(''); + $assert->statusCodeEquals(200); + $assert->linkByHrefExists('examples/email-example'); + + // Verify if we can successfully access the email_example page. + $this->drupalGet('examples/email-example'); + $assert->statusCodeEquals(200); + + // Verifiy email form has email & message fields. + $assert->fieldValueEquals('edit-email', NULL); + $assert->fieldValueEquals('edit-message', NULL); + + // Verifiy email form is submitted. + $edit = ['email' => 'example@example.com', 'message' => 'test']; + $this->drupalPostForm('examples/email-example', $edit, 'Submit'); + $assert->statusCodeEquals(200); + + // Verifiy comfirmation page. + $assert->pageTextContains('Your message has been sent.'); + $this->assertMailString('to', $edit['email'], 1); + + // Verifiy correct email recieved. + $from = \Drupal::config('system.site')->get('mail'); + $this->assertMailString('subject', "E-mail sent from $from", 1); + $this->assertMailString('body', $edit['message'], 1); + $this->assertMailString('body', "\n--\nMail altered by email_example module.", 1); + } + +} diff --git a/sites/all/modules/examples/examples/events_example/events_example.info.yml b/sites/all/modules/examples/examples/events_example/events_example.info.yml new file mode 100644 index 000000000..940cb520f --- /dev/null +++ b/sites/all/modules/examples/examples/events_example/events_example.info.yml @@ -0,0 +1,13 @@ +name: Events Example +type: module +description: Provides an example of subscribing to and dispatching events. +package: Example modules +# core: 8.x +dependencies: + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/events_example/events_example.links.menu.yml b/sites/all/modules/examples/examples/events_example/events_example.links.menu.yml new file mode 100644 index 000000000..61eb582a0 --- /dev/null +++ b/sites/all/modules/examples/examples/events_example/events_example.links.menu.yml @@ -0,0 +1,4 @@ +events_example.description: + title: 'Events Example' + description: 'Example of dispatching and subscribing to events.' + route_name: events_example.description diff --git a/sites/all/modules/examples/examples/events_example/events_example.module b/sites/all/modules/examples/examples/events_example/events_example.module new file mode 100644 index 000000000..0bfac70fe --- /dev/null +++ b/sites/all/modules/examples/examples/events_example/events_example.module @@ -0,0 +1,75 @@ +type = $type; + $this->report = $report; + } + + /** + * Get the incident type. + * + * @return string + * The type of report. + */ + public function getType() { + return $this->type; + } + + /** + * Get the detailed incident report. + * + * @return string + * The text of the report. + */ + public function getReport() { + return $this->report; + } + +} diff --git a/sites/all/modules/examples/examples/events_example/src/EventSubscriber/EventsExampleSubscriber.php b/sites/all/modules/examples/examples/events_example/src/EventSubscriber/EventsExampleSubscriber.php new file mode 100644 index 000000000..c78cfd925 --- /dev/null +++ b/sites/all/modules/examples/examples/events_example/src/EventSubscriber/EventsExampleSubscriber.php @@ -0,0 +1,111 @@ +getType() == 'stolen_princess') { + drupal_set_message($this->t('Mario has been alerted. Thank you. This message was set by an event subscriber. See \Drupal\events_example\EventSubscriber\EventsExampleSubscriber::notifyMario()'), 'status'); + } + } + + /** + * Let Batman know about any events involving the Joker. + * + * @param \Drupal\events_example\Event\IncidentReportEvent $event + * The event object containing the incident report. + */ + public function notifyBatman(IncidentReportEvent $event) { + if ($event->getType() == 'joker') { + drupal_set_message($this->t('Batman has been alerted. Thank you. This message was set by an event subscriber. See \Drupal\events_example\EventSubscriber\EventsExampleSubscriber::notifyBatman()'), 'status'); + // Optionally use the event object to stop propagation. + // If there are other subscribers that have not been called yet this will + // cause them to be skipped. + $event->stopPropagation(); + } + } + +} diff --git a/sites/all/modules/examples/examples/events_example/src/Form/EventsExampleForm.php b/sites/all/modules/examples/examples/events_example/src/Form/EventsExampleForm.php new file mode 100644 index 000000000..edc0f460c --- /dev/null +++ b/sites/all/modules/examples/examples/events_example/src/Form/EventsExampleForm.php @@ -0,0 +1,130 @@ +eventDispatcher = $event_dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('event_dispatcher') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['intro'] = [ + '#markup' => '

' . $this->t('This form demonstrates subscribing to, and dispatching, events. When the form is submitted an event is dispatched indicating a new report has been submitted. Event subscribers respond to this event with various messages depending on the incident type. Review the code for the events_example module to see how it works.') . '

', + ]; + + $form['incident_type'] = [ + '#type' => 'radios', + '#required' => TRUE, + '#title' => t('What type of incident do you want to report?'), + '#options' => [ + 'stolen_princess' => $this->t('Missing princess'), + 'cat' => $this->t('Cat stuck in tree'), + 'joker' => $this->t('Something involving the Joker'), + ], + ]; + + $form['incident'] = [ + '#type' => 'textarea', + '#required' => FALSE, + '#title' => t('Incident report'), + '#description' => t('Describe the incident in detail. This information will be passed along to all crime fighters.'), + '#cols' => 60, + '#rows' => 5, + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'events_example_form'; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $type = $form_state->getValue('incident_type'); + $report = $form_state->getValue('incident'); + + // When dispatching, or triggering, an event start by constructing a new + // event object. Then use the event dispatcher service to notify any event + // subscribers. Event objects are used to transport relevant data to any + // subscribers, as well as keep track of the current state of an event. It + // is best practice to create a unique class wrapping + // \Symfony\Component\EventDispatcher\Event. + $event = new IncidentReportEvent($type, $report); + + // Dispatch an event by specifying which event, and providing an event + // object. Rather than hard code the event name you should use a constant + // to represent the event being dispatched. The constant serves as a + // location for documentation of the event, and ensures your code is future + // proofed against event name changes. + $this->eventDispatcher->dispatch(IncidentEvents::NEW_REPORT, $event); + } + +} diff --git a/sites/all/modules/examples/examples/events_example/tests/src/Functional/EventsExampleTest.php b/sites/all/modules/examples/examples/events_example/tests/src/Functional/EventsExampleTest.php new file mode 100644 index 000000000..57bc9f5f3 --- /dev/null +++ b/sites/all/modules/examples/examples/events_example/tests/src/Functional/EventsExampleTest.php @@ -0,0 +1,64 @@ +drupalGet('examples/events-example'); + $this->assertSession()->statusCodeEquals(200); + + // Verify the page contains the required form fields. + $this->assertSession()->fieldExists('incident_type'); + $this->assertSession()->fieldExists('incident'); + + // Submit the form with an incident type of 'stolen_princess'. This does a + // couple of things. Fist of all, it ensures that our code in + // EventsExampleForm::submitForm() that dispatches events works. If it did + // not work, no event would be dispatched, and the message below would never + // get displayed. Secondly, it tests that our + // EventsExampleSubscriber::notifyMario() event subscriber is triggered for + // incidents of the type 'stolen_princess'. + $values = [ + 'incident_type' => 'stolen_princess', + 'incident' => $this->randomString(), + ]; + $this->drupalPostForm('examples/events-example', $values, 'Submit'); + $this->assertSession()->pageTextContains('Mario has been alerted. Thank you.'); + + // Fill out the form again, this time testing that the + // EventsExampleSubscriber::notifyBatman() subscriber is working. + $values = [ + 'incident_type' => 'joker', + 'incident' => $this->randomString(), + ]; + $this->drupalPostForm('examples/events-example', $values, 'Submit'); + $this->assertSession()->pageTextContains('Batman has been alerted. Thank you.'); + } + +} diff --git a/sites/all/modules/examples/examples/events_example/tests/src/Kernel/EventsExampleServiceTest.php b/sites/all/modules/examples/examples/events_example/tests/src/Kernel/EventsExampleServiceTest.php new file mode 100644 index 000000000..409f6d927 --- /dev/null +++ b/sites/all/modules/examples/examples/events_example/tests/src/Kernel/EventsExampleServiceTest.php @@ -0,0 +1,29 @@ +container->get('events_example_subscriber'); + $this->assertInstanceOf(EventsExampleSubscriber::class, $subscriber); + } + +} diff --git a/sites/all/modules/examples/examples/examples.info.yml b/sites/all/modules/examples/examples/examples.info.yml new file mode 100644 index 000000000..6ca2e2872 --- /dev/null +++ b/sites/all/modules/examples/examples/examples.info.yml @@ -0,0 +1,13 @@ +name: Examples for Developers +type: module +description: 'Provides example code for you to learn from.' +package: Example modules +# core: 8.x +dependencies: + - drupal:toolbar + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/examples.libraries.yml b/sites/all/modules/examples/examples/examples.libraries.yml new file mode 100644 index 000000000..e63351a57 --- /dev/null +++ b/sites/all/modules/examples/examples/examples.libraries.yml @@ -0,0 +1,5 @@ +examples.icons: + version: VERSION + css: + theme: + css/examples.icons.css: {} diff --git a/sites/all/modules/examples/examples/examples.module b/sites/all/modules/examples/examples/examples.module new file mode 100644 index 000000000..96cb44ffb --- /dev/null +++ b/sites/all/modules/examples/examples/examples.module @@ -0,0 +1,131 @@ + 'ajax_example.description', + 'batch_example' => 'batch_example.form', + 'block_example' => 'block_example.description', + 'cache_example' => 'cache_example.description', + 'config_entity_example' => 'entity.robot.list', + 'content_entity_example' => 'entity.content_entity_example_contact.collection', + 'cron_example' => 'cron_example', + 'dbtng_example' => 'dbtng_example', + 'email_example' => 'email_example.description', + 'events_example' => 'events_example.description', + 'form_api_example' => 'form_api_example.description', + 'field_example' => 'field_example.description', + 'field_permission_example' => 'field_permission_example.description', + 'file_example' => 'file_example.fileapi', + 'hooks_example' => 'hooks_example.description', + 'js_example' => 'js_example.info', + 'node_type_example' => 'config_node_type_example.description', + 'page_example' => 'page_example_description', + 'pager_example' => 'pager_example.page', + 'phpunit_example' => 'phpunit_example_description', + 'plugin_type_example' => 'plugin_type_example.description', + 'simpletest_example' => 'simpletest_example_description', + 'tabledrag_example' => 'tabledrag_example.description', + 'stream_wrapper_example' => 'stream_wrapper_example.description', + 'testing_example' => 'testing_example.description', + 'queue_example' => 'queue_example', + 'tablesort_example' => 'tablesort_example_description', + 'tour_example' => 'tour_example_description', + ]; + + // Build a list of links for the menu. + $links = []; + foreach ($examples as $module => $route) { + // Get the module info (title, description) from Drupal. + $info = system_get_info('module', $module); + + // If there's no info, the example isn't enabled, so don't display it. + if (!empty($info)) { + $links[$module] = [ + 'title' => t($info['name']), + 'url' => Url::fromRoute($route), + 'attributes' => [ + 'title' => t($info['description']), + ], + ]; + } + } + + // Add a link to enable all examples. + $links['enable_examples'] = [ + 'title' => t('Enable Examples'), + 'url' => Url::fromRoute('system.modules_list'), + 'options' => [ + 'title' => t('Enable more examples in on the Extend page.'), + ], + 'fragment' => 'edit-modules-example-modules', + ]; + + // Create the examples toolbar render array. + $items['examples'] = [ + '#type' => 'toolbar_item', + 'tab' => [ + '#type' => 'link', + '#title' => t('Examples'), + '#url' => Url::fromRoute(''), + '#attributes' => [ + 'title' => t('Developer Examples'), + 'class' => ['toolbar-icon', 'toolbar-icon-examples'], + ], + ], + 'tray' => [ + '#heading' => t('Developer Examples'), + 'shortcuts' => [ + '#theme' => 'links__toolbar_example', + '#links' => $links, + '#attributes' => [ + 'class' => ['toolbar-menu'], + ], + ], + ], + '#weight' => 99, + '#attached' => [ + 'library' => [ + 'examples/examples.icons', + ], + ], + ]; + + return $items; +} + +/** + * @} End of 'defgroup examples'. + */ diff --git a/sites/all/modules/examples/examples/field_example/config/schema/field_example.schema.yml b/sites/all/modules/examples/examples/field_example/config/schema/field_example.schema.yml new file mode 100644 index 000000000..c2e40ea05 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/config/schema/field_example.schema.yml @@ -0,0 +1,12 @@ +# @see Configuration schema/metadata at https://drupal.org/node/1905070 +# +field.field_example_rgb.value: + type: sequence + label: 'Default value' + sequence: + - type: mapping + label: 'Default' + mapping: + value: + type: string + label: 'Value' diff --git a/sites/all/modules/examples/examples/field_example/field_example.info.yml b/sites/all/modules/examples/examples/field_example/field_example.info.yml new file mode 100644 index 000000000..954a19ff2 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/field_example.info.yml @@ -0,0 +1,15 @@ +name: Field Example +type: module +description: Demonstrates the Field API via some field plugin implementations. +package: Example modules +# core: 8.x +dependencies: + - drupal:color + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/field_example/field_example.js b/sites/all/modules/examples/examples/field_example/field_example.js new file mode 100644 index 000000000..1edb9ec55 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/field_example.js @@ -0,0 +1,27 @@ +/** + * @file + * Javascript for Field Example. + */ + +/** + * Provides a farbtastic colorpicker for the fancier widget. + */ +(function ($) { + + 'use strict'; + + Drupal.behaviors.field_example_colorpicker = { + attach: function () { + $('.edit-field-example-colorpicker').on('focus', function (event) { + var edit_field = this; + var picker = $(this).closest('div').parent().find('.field-example-colorpicker'); + // Hide all color pickers except this one. + $('.field-example-colorpicker').hide(); + $(picker).show(); + $.farbtastic(picker, function (color) { + edit_field.value = color; + }).setColor(edit_field.value); + }); + } + }; +})(jQuery); diff --git a/sites/all/modules/examples/examples/field_example/field_example.libraries.yml b/sites/all/modules/examples/examples/field_example/field_example.libraries.yml new file mode 100644 index 000000000..7331ff916 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/field_example.libraries.yml @@ -0,0 +1,4 @@ +colorpicker: + version: 1.x + js: + field_example.js: {} diff --git a/sites/all/modules/examples/examples/field_example/field_example.links.menu.yml b/sites/all/modules/examples/examples/field_example/field_example.links.menu.yml new file mode 100644 index 000000000..07b81b3be --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/field_example.links.menu.yml @@ -0,0 +1,3 @@ +field_example.description: + title: Field Example + route_name: field_example.description diff --git a/sites/all/modules/examples/examples/field_example/field_example.module b/sites/all/modules/examples/examples/field_example/field_example.module new file mode 100644 index 000000000..7f005ab73 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/field_example.module @@ -0,0 +1,34 @@ +administratorAccount = $this->drupalCreateUser($permissions); + parent::drupalLogin($this->administratorAccount); + + // Prepare a new content type where the field will be added. + $this->contentTypeName = strtolower($this->randomMachineName(10)); + $this->drupalGet('admin/structure/types/add'); + $edit = [ + 'name' => $this->contentTypeName, + 'type' => $this->contentTypeName, + ]; + $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); + $this->assertText(t('The content type @name has been added.', ['@name' => $this->contentTypeName])); + + // Reset the permission cache. + $create_permission = 'create ' . $this->contentTypeName . ' content'; + $this->checkPermissions([$create_permission], TRUE); + + // Now that we have a new content type, create a user that has privileges + // on the content type. + $this->authorAccount = $this->drupalCreateUser([$create_permission]); + } + + /** + * Create a field on the content type created during setUp(). + * + * @param string $type + * The storage field type to create. + * @param string $widget_type + * The widget to use when editing this field. + * @param int|string $cardinality + * Cardinality of the field. Use -1 to signify 'unlimited'. + * @param string $fieldFormatter + * The formatter to use when editing this field. + * + * @return string + * Name of the field, like field_something + */ + protected function createField($type = 'field_example_rgb', $widget_type = 'field_example_text', $cardinality = '1', $fieldFormatter = 'field_example_simple_text') { + $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/fields'); + + // Go to the 'Add field' page. + $this->clickLink('Add field'); + + // Make a name for this field. + $field_name = strtolower($this->randomMachineName(10)); + + // Fill out the field form. + $edit = [ + 'new_storage_type' => $type, + 'field_name' => $field_name, + 'label' => $field_name, + ]; + $this->drupalPostForm(NULL, $edit, t('Save and continue')); + + // Fill out the $cardinality form as if we're not using an unlimited number + // of values. + $edit = [ + 'cardinality' => 'number', + 'cardinality_number' => (string) $cardinality, + ]; + // If we have -1 for $cardinality, we should change the form's drop-down + // from 'Number' to 'Unlimited'. + if (-1 == $cardinality) { + $edit = [ + 'cardinality' => '-1', + 'cardinality_number' => '1', + ]; + } + + // And now we save the cardinality settings. + $this->drupalPostForm(NULL, $edit, t('Save field settings')); + debug( + t('Saved settings for field %field_name with widget %widget_type and cardinality %cardinality', + [ + '%field_name' => $field_name, + '%widget_type' => $widget_type, + '%cardinality' => $cardinality, + ] + ) + ); + $this->assertText(t('Updated field @name field settings.', ['@name' => $field_name])); + + // Set the widget type for the newly created field. + $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/form-display'); + $edit = [ + 'fields[field_' . $field_name . '][type]' => $widget_type, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + + // Set the field formatter for the newly created field. + $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/display'); + $edit1 = [ + 'fields[field_' . $field_name . '][type]' => $fieldFormatter, + ]; + $this->drupalPostForm(NULL, $edit1, t('Save')); + + return $field_name; + } + +} diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroudFormatter.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroudFormatter.php new file mode 100644 index 000000000..581bce3a3 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroudFormatter.php @@ -0,0 +1,40 @@ + $item) { + $elements[$delta] = [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => t('The content area color has been changed to @code', ['@code' => $item->value]), + '#attributes' => [ + 'style' => 'background-color: ' . $item->value, + ], + ]; + } + return $elements; + } + +} diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php new file mode 100644 index 000000000..6a382a470 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php @@ -0,0 +1,45 @@ + $item) { + $elements[$delta] = [ + // We create a render array to produce the desired markup, + // "

The color code ... #hexcolor

". + // See theme_html_tag(). + '#type' => 'html_tag', + '#tag' => 'p', + '#attributes' => [ + 'style' => 'color: ' . $item->value, + ], + '#value' => $this->t('The color code in this field is @code', ['@code' => $item->value]), + ]; + } + + return $elements; + } + +} diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldType/RgbItem.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldType/RgbItem.php new file mode 100644 index 000000000..55c0617e1 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldType/RgbItem.php @@ -0,0 +1,55 @@ + [ + 'value' => [ + 'type' => 'text', + 'size' => 'tiny', + 'not null' => FALSE, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $value = $this->get('value')->getValue(); + return $value === NULL || $value === ''; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties['value'] = DataDefinition::create('string') + ->setLabel(t('Hex value')); + + return $properties; + } + +} diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/ColorPickerWidget.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/ColorPickerWidget.php new file mode 100644 index 000000000..ebe07b8bb --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/ColorPickerWidget.php @@ -0,0 +1,43 @@ + '
', + '#attributes' => ['class' => ['edit-field-example-colorpicker']], + '#attached' => [ + // Add Farbtastic color picker and javascript file to trigger the + // colorpicker. + 'library' => [ + 'core/jquery.farbtastic', + 'field_example/colorpicker', + ], + ], + ]; + + return $element; + } + +} diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/Text3Widget.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/Text3Widget.php new file mode 100644 index 000000000..f8690a4c0 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/Text3Widget.php @@ -0,0 +1,87 @@ +value) ? $items[$delta]->value : ''; + // Parse the single hex string into RBG values. + if (!empty($value)) { + preg_match_all('@..@', substr($value, 1), $match); + } + else { + $match = [[]]; + } + + // Set up the form element for this widget. + $element += [ + '#type' => 'details', + '#element_validate' => [ + [$this, 'validate'], + ], + ]; + + // Add in the RGB textfield elements. + foreach (['r' => t('Red'), 'g' => t('Green'), 'b' => t('Blue')] as $key => $title) { + $element[$key] = [ + '#type' => 'textfield', + '#title' => $title, + '#size' => 2, + '#default_value' => array_shift($match[0]), + '#attributes' => ['class' => ['rgb-entry']], + '#description' => t('The 2-digit hexadecimal representation of @color saturation, like "a1" or "ff"', ['@color' => $title]), + ]; + // Since Form API doesn't allow a fieldset to be required, we + // have to require each field element individually. + if ($element['#required']) { + $element[$key]['#required'] = TRUE; + } + } + return ['value' => $element]; + } + + /** + * Validate the fields and convert them into a single value as text. + */ + public function validate($element, FormStateInterface $form_state) { + // Validate each of the textfield entries. + $values = []; + foreach (['r', 'g', 'b'] as $colorfield) { + $values[$colorfield] = $element[$colorfield]['#value']; + // If they left any empty, we'll set the value empty and quit. + if (strlen($values[$colorfield]) == 0) { + $form_state->setValueForElement($element, ''); + return; + } + // If they gave us anything that's not hex, reject it. + if ((strlen($values[$colorfield]) != 2) || !ctype_xdigit($values[$colorfield])) { + $form_state->setError($element[$colorfield], $form_state, t("Saturation value must be a 2-digit hexadecimal value between 00 and ff.")); + } + } + + // Set the value of the entire form element. + $value = strtolower(sprintf('#%02s%02s%02s', $values['r'], $values['g'], $values['b'])); + $form_state->setValueForElement($element, $value); + } + +} diff --git a/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/TextWidget.php b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/TextWidget.php new file mode 100644 index 000000000..fcebc2d4f --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/src/Plugin/Field/FieldWidget/TextWidget.php @@ -0,0 +1,54 @@ +value) ? $items[$delta]->value : ''; + $element += [ + '#type' => 'textfield', + '#default_value' => $value, + '#size' => 7, + '#maxlength' => 7, + '#element_validate' => [ + [$this, 'validate'], + ], + ]; + return ['value' => $element]; + } + + /** + * Validate the color text field. + */ + public function validate($element, FormStateInterface $form_state) { + $value = $element['#value']; + if (strlen($value) == 0) { + $form_state->setValueForElement($element, ''); + return; + } + if (!preg_match('/^#([a-f0-9]{6})$/iD', strtolower($value))) { + $form_state->setError($element, t("Color must be a 6-digit hexadecimal value, suitable for CSS.")); + } + } + +} diff --git a/sites/all/modules/examples/examples/field_example/templates/description.html.twig b/sites/all/modules/examples/examples/field_example/templates/description.html.twig new file mode 100644 index 000000000..50199bf16 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/templates/description.html.twig @@ -0,0 +1,12 @@ +{# + +Description text for the Field Example. + +#} + +{% trans %} + +

The Field Example provides a field composed of an HTML RGB value, like + #ff00ff. To use it, add the field to a content type.

+ +{% endtrans %} diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorBackgroundFormatterTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorBackgroundFormatterTest.php new file mode 100644 index 000000000..4cf56246d --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorBackgroundFormatterTest.php @@ -0,0 +1,98 @@ +assertSession(); + // Login with Admin and create a field. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '1', 'field_example_color_background'); + + // Login with Author user for content creation. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Details to be submitted for content creation. + $title = $this->randomMachineName(20); + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value]' => '#00ff00', + ]; + + // Submit the content creation form. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify color. + $assert->pageTextContains('The content area color has been changed to #00ff00'); + } + + /** + * Tests a multi-value field. + * + * Test the following scenarios in Field example: + * - Creates a content type. + * - Adds a multivalued field_example_rgb to it. + * - Creates a node of the new type. + * - Populates the multivalued field with two items. + * - Tests the result. + */ + public function testMultiValueField() { + $assert = $this->assertSession(); + + // Login with Admin and create a field. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '-1', 'field_example_color_background'); + + // Login with Author user for content creation. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Details to be submitted for content creation. + $title = $this->randomMachineName(20); + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value]' => '#00ff00', + ]; + + // Add another field value. + $this->drupalPostForm(NULL, $edit, 'Add another item'); + + // Set value for newly added item. + $edit = [ + 'field_' . $this->fieldName . '[1][value]' => '#ffff4f', + ]; + + // Submit the content creation form. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify color. + $assert->pageTextContains('The content area color has been changed to #00ff00'); + $assert->pageTextContains('The content area color has been changed to #ffff4f'); + } + +} diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorPickerWidgetTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorPickerWidgetTest.php new file mode 100644 index 000000000..26626cc93 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/ColorPickerWidgetTest.php @@ -0,0 +1,99 @@ +assertSession(); + // Login with Admin and create a field. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '1'); + + // Login with Author user for content creation. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Details to be submitted for content creation. + $title = $this->randomMachineName(20); + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value]' => '#00ff00', + ]; + + // Submit the content creation form. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify color. + $assert->pageTextContains('The color code in this field is #00ff00'); + } + + /** + * Field example scenario tests. + * + * The following scenarios: + * - Creates a content type. + * - Adds a multivalued field_example_rgb to it. + * - Creates a node of the new type. + * - Populates the multivalued field with two items. + * - Tests the result. + */ + public function testMultiValueField() { + $assert = $this->assertSession(); + + // Login with Admin and create a field. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_colorpicker', '-1'); + + // Login with Author user for content creation. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Details to be submitted for content creation. + $title = $this->randomMachineName(20); + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value]' => '#00ff00', + ]; + + // Add another field value. + $this->drupalPostForm(NULL, $edit, 'Add another item'); + + // Set value for newly added item. + $edit = [ + 'field_' . $this->fieldName . '[1][value]' => '#ffffff', + ]; + + // Submit the content creation form. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify color. + $assert->pageTextContains('The color code in this field is #00ff00'); + $assert->pageTextContains('The color code in this field is #ffffff'); + } + +} diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleBrowserTestBase.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleBrowserTestBase.php new file mode 100644 index 000000000..0a8b1236d --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleBrowserTestBase.php @@ -0,0 +1,167 @@ +administratorAccount = $this->drupalCreateUser($permissions); + parent::drupalLogin($this->administratorAccount); + + // Prepare a new content type where the field will be added. + $this->contentTypeName = strtolower($this->randomMachineName(10)); + $this->drupalGet('admin/structure/types/add'); + $edit = [ + 'name' => $this->contentTypeName, + 'type' => $this->contentTypeName, + ]; + $this->drupalPostForm(NULL, $edit, t('Save and manage fields')); + $this->assertText(t('The content type @name has been added.', ['@name' => $this->contentTypeName])); + + // Reset the permission cache. + $create_permission = 'create ' . $this->contentTypeName . ' content'; + $this->checkPermissions([$create_permission], TRUE); + + // Now that we have a new content type, create a user that has privileges + // on the content type. + $this->authorAccount = $this->drupalCreateUser([$create_permission]); + } + + /** + * Create a field on the content type created during setUp(). + * + * @param string $type + * The storage field type to create. + * @param string $widget_type + * The widget to use when editing this field. + * @param int|string $cardinality + * Cardinality of the field. Use -1 to signify 'unlimited'. + * @param string $fieldFormatter + * The formatter to use when editing this field. + * + * @return string + * Name of the field, like field_something + */ + protected function createField($type = 'field_example_rgb', $widget_type = 'field_example_text', $cardinality = '1', $fieldFormatter = 'field_example_simple_text') { + $assert = $this->assertSession(); + + $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/fields'); + + // Go to the 'Add field' page. + $this->clickLink('Add field'); + + // Make a name for this field. + $field_name = strtolower($this->randomMachineName(10)); + + // Fill out the field form. + $edit = [ + 'new_storage_type' => $type, + 'field_name' => $field_name, + 'label' => $field_name, + ]; + $this->drupalPostForm(NULL, $edit, t('Save and continue')); + + // Fill out the $cardinality form as if we're not using an unlimited number + // of values. + $edit = [ + 'cardinality' => 'number', + 'cardinality_number' => (string) $cardinality, + ]; + // If we have -1 for $cardinality, we should change the form's drop-down + // from 'Number' to 'Unlimited'. + if (-1 == $cardinality) { + $edit = [ + 'cardinality' => '-1', + 'cardinality_number' => '1', + ]; + } + + // And now we save the cardinality settings. + $this->drupalPostForm(NULL, $edit, t('Save field settings')); + $this->verbose( + t('Saved settings for field %field_name with widget %widget_type and cardinality %cardinality', + [ + '%field_name' => $field_name, + '%widget_type' => $widget_type, + '%cardinality' => $cardinality, + ] + ) + ); + $assert->pageTextContains(t('Updated field @name field settings.', ['@name' => $field_name])); + + // Set the widget type for the newly created field. + $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/form-display'); + $edit = [ + 'fields[field_' . $field_name . '][type]' => $widget_type, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + + // Set the field formatter for the newly created field. + $this->drupalGet('admin/structure/types/manage/' . $this->contentTypeName . '/display'); + $edit1 = [ + 'fields[field_' . $field_name . '][type]' => $fieldFormatter, + ]; + $this->drupalPostForm(NULL, $edit1, t('Save')); + + return $field_name; + } + +} diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleMenuTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleMenuTest.php new file mode 100644 index 000000000..bd53c3626 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/FieldExampleMenuTest.php @@ -0,0 +1,50 @@ +assertSession(); + $this->drupalGet(''); + $assert->linkByHrefExists('examples/field-example'); + } + + /** + * Tests field_example menus. + */ + public function testBlockExampleMenu() { + $assert = $this->assertSession(); + $this->drupalGet('examples/field-example'); + $assert->statusCodeEquals(200); + } + +} diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/Text3WidgetTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/Text3WidgetTest.php new file mode 100644 index 000000000..93e3b1408 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/Text3WidgetTest.php @@ -0,0 +1,104 @@ +assertSession(); + // Add a single field as administrator user. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_3text', '1'); + // Post-condition: Content type now has the desired field. + // Switch to the author user to create content with this type and field. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Fill the create form. + $title = 'test_title'; + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value][r]' => '00', + 'field_' . $this->fieldName . '[0][value][g]' => '0a', + 'field_' . $this->fieldName . '[0][value][b]' => '01', + ]; + + // Create the content. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify the value is shown when viewing this node. + $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/p"); + $this->assertEquals("The color code in this field is #000a01", (string) $field_p[0]->getText()); + } + + /** + * Test basic functionality of the example field. + * + * - Creates a content type. + * - Adds a single-valued field_example_rgb to it. + * - Adds a multivalued field_example_rgb to it. + * - Creates a node of the new type. + * - Populates the single-valued field. + * - Populates the multivalued field with two items. + * - Tests the result. + */ + public function testMultiValueField() { + $assert = $this->assertSession(); + + // Add a single field as administrator user. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_3text', '-1'); + // Post-condition: Content type now has the desired field. + // Switch to the author user to create content with this type and field. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Fill the create form. + $title = $this->randomMachineName(20); + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value][r]' => '00', + 'field_' . $this->fieldName . '[0][value][g]' => 'ff', + 'field_' . $this->fieldName . '[0][value][b]' => '00', + ]; + + // Add a 2nd item to the multivalue field, so hit "add another". + $this->drupalPostForm(NULL, $edit, 'Add another item'); + $edit = [ + 'field_' . $this->fieldName . '[1][value][r]' => 'ff', + 'field_' . $this->fieldName . '[1][value][g]' => 'ff', + 'field_' . $this->fieldName . '[1][value][b]' => 'ff', + ]; + + // Create the content. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify the values are shown when viewing this node. + $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/div/p"); + $this->assertEquals('The color code in this field is #00ff00', (string) $field_p[0]->getText()); + $this->assertEquals('The color code in this field is #ffffff', (string) $field_p[1]->getText()); + } + +} diff --git a/sites/all/modules/examples/examples/field_example/tests/src/Functional/TextWidgetTest.php b/sites/all/modules/examples/examples/field_example/tests/src/Functional/TextWidgetTest.php new file mode 100644 index 000000000..cd4169465 --- /dev/null +++ b/sites/all/modules/examples/examples/field_example/tests/src/Functional/TextWidgetTest.php @@ -0,0 +1,104 @@ +assertSession(); + + // Add a single field as administrator user. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_text', '1'); + + // Now that we have a content type with the desired field, switch to the + // author user to create content with it. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Add a node. + $title = $this->randomMachineName(20); + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value]' => '#000001', + ]; + + // Create the content. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify the value is shown when viewing this node. + $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/p"); + $this->assertEquals('The color code in this field is #000001', (string) $field_p[0]->getText()); + } + + /** + * Test basic functionality of the example field. + * + * - Creates a content type. + * - Adds a single-valued field_example_rgb to it. + * - Adds a multivalued field_example_rgb to it. + * - Creates a node of the new type. + * - Populates the single-valued field. + * - Populates the multivalued field with two items. + * - Tests the result. + */ + public function testMultiValueField() { + $assert = $this->assertSession(); + + // Add a single field as administrator user. + $this->drupalLogin($this->administratorAccount); + $this->fieldName = $this->createField('field_example_rgb', 'field_example_text', '-1'); + + // Now that we have a content type with the desired field, switch to the + // author user to create content with it. + $this->drupalLogin($this->authorAccount); + $this->drupalGet('node/add/' . $this->contentTypeName); + + // Add a node. + $title = $this->randomMachineName(20); + $edit = [ + 'title[0][value]' => $title, + 'field_' . $this->fieldName . '[0][value]' => '#00ff00', + ]; + + // We want to add a 2nd item to the multivalue field, so hit "add another". + $this->drupalPostForm(NULL, $edit, 'Add another item'); + + $edit = [ + 'field_' . $this->fieldName . '[1][value]' => '#ffffff', + ]; + + // Now we can fill in the second item in the multivalue field and save. + $this->drupalPostForm(NULL, $edit, 'Save'); + $assert->pageTextContains(t('@type @title has been created', ['@type' => $this->contentTypeName, '@title' => $title])); + + // Verify the value is shown when viewing this node. + $field_p = $this->xpath("//div[contains(@class,'field--type-field-example-rgb')]/div/div/p"); + $this->assertEquals('The color code in this field is #00ff00', (string) $field_p[0]->getText()); + $this->assertEquals('The color code in this field is #ffffff', (string) $field_p[1]->getText()); + } + +} diff --git a/sites/all/modules/examples/examples/field_permission_example/css/field-permission-description.css b/sites/all/modules/examples/examples/field_permission_example/css/field-permission-description.css new file mode 100644 index 000000000..a366cc58d --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/css/field-permission-description.css @@ -0,0 +1,3 @@ +/** + * Field Permssions Example CSS + */ diff --git a/sites/all/modules/examples/examples/field_permission_example/css/field_permission_example.css b/sites/all/modules/examples/examples/field_permission_example/css/field_permission_example.css new file mode 100644 index 000000000..5bd0b0572 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/css/field_permission_example.css @@ -0,0 +1,20 @@ +/** + * @file + * CSS for Field Example. + */ +.stickynote { + background: #fefabc; + padding: 0.8em; + font-family: cursive; + font-size: 1.1em; + color: #def9ff; + width: 15em; + -moz-transform: rotate(2deg); + -webkit-transform: rotate(2deg); + -o-transform: rotate(2deg); + -ms-transform: rotate(2deg); + transform: rotate(2deg); + -moz-box-shadow: 0 4px 6px #333; + -webkit-box-shadow: 0 4px 6px #333; + box-shadow: 0 4px 6px #333; +} diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.info.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.info.yml new file mode 100644 index 000000000..cd906b500 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.info.yml @@ -0,0 +1,14 @@ +name: Field Permission Example +type: module +description: Demonstrates how to create a field and implement access control over it. +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.libraries.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.libraries.yml new file mode 100644 index 000000000..9b4e619b1 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.libraries.yml @@ -0,0 +1,10 @@ +fieldnote_sticky: + version: 1.x + css: + theme: + css/field_permission_example.css: {} +field_permissions_description: + version: 1.x + css: + theme: + css/field-permission-description.css: {} diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.links.menu.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.links.menu.yml new file mode 100644 index 000000000..ce9a56b3d --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.links.menu.yml @@ -0,0 +1,3 @@ +field_permission_example.description: + title: Field Permission Example + route_name: field_permission_example.description diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.module b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.module new file mode 100644 index 000000000..be38dfbf0 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.module @@ -0,0 +1,196 @@ +hasPermission(). We also give special edit + * access to users with the 'bypass node access', 'administer content types' + * permissions, defined by the node module, and the 'administer the fieldnote + * field' we define for the module. + * + * One tricky part is that our field won't always be attached to nodes. It could + * be attached to any type of entity. Fortunately, most content entities + * implement EntityOwnerInterface, which gives us a way to check this. An + * exception to this is the User entity; here, we just check to see that the + * account name matches that of $account. We can get the entity itself by + * calling $items->getEntity(), since these "know" what entity they belong to. + * + * In a real application, we'd have use-case specific permissions which might be + * more complex than these. Or perhaps simpler. + * + * You can see a more complex field implementation in field_example.module. + * + * @see field_example + * @see field_example.module + * @see field_types + * @see field + */ + +// Use statements to support hook_entity_field_access. +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Access\AccessResult; + +// Interfaces used by entities to declare "ownership". +use Drupal\user\EntityOwnerInterface; +use Drupal\user\UserInterface; + +// Use statements for hook_entity_test_access. +use Drupal\Core\Entity\EntityInterface; + +/** + * Implements hook_entity_field_access(). + * + * We want to make sure that fields aren't being seen or edited + * by those who shouldn't. + */ +function field_permission_example_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { + // Find out what field we're looking at. If it isn't + // our sticky note widget, tell Drupal we don't care about its access. + if ($field_definition->getType() != 'field_permission_example_fieldnote') { + return AccessResult::neutral(); + } + + // First we'll check if the user has the 'superuser' + // permissions that node provides. This way administrators + // will be able to administer the content types. + if ($account->hasPermission('bypass node access')) { + drupal_set_message(t('User can bypass node access.')); + return AccessResult::allowed(); + } + if ($account->hasPermission('administer content types', $account)) { + drupal_set_message(t('User can administer content types.')); + return AccessResult::allowed(); + } + if ($account->hasPermission('administer the fieldnote field', $account)) { + drupal_set_message(t('User can administer this field.')); + return AccessResult::allowed(); + } + + // For anyone else, it depends on the desired operation. + if ($operation == 'view' and $account->hasPermission('view any fieldnote')) { + drupal_set_message(t('User can view any field note.')); + return AccessResult::allowed(); + } + + if ($operation == 'edit' and $account->hasPermission('edit any fieldnote')) { + drupal_set_message(t('User can edit any field note.')); + return AccessResult::allowed(); + } + + // At this point, we need to know if the user "owns" the entity we're attached + // to. If it's a user, we'll use the account name to test. Otherwise rely on + // the entity implementing the EntityOwnerInterface. Anything else can't be + // owned, and we'll refuse access. + if ($items) { + $entity = $items->getEntity(); + if ((($entity instanceof EntityOwnerInterface) and + $entity->getOwner()->getAccountName() == $account->getAccountName()) or + (($entity instanceof UserInterface) and + $entity->name->value == $account->getAccountName()) + ) { + if ($operation == 'view' and $account->hasPermission('view own fieldnote')) { + drupal_set_message(t('User can view their own field note.')); + return AccessResult::allowed(); + } + if ($operation == 'edit' and $account->hasPermission('edit own fieldnote')) { + drupal_set_message(t('User can edit their own field note.')); + return AccessResult::allowed(); + } + } + } + // Anything else on this field is forbidden. + return AccessResult::forbidden(); +} + + +/** + * Implements hook_ENTITY_TYPE_access(). + * + * Note: this routine is added so we can more easily test our access code. Core + * defines an entity_test entity that is used for testing fields in core. We add + * this routine to make the entity_test entity editable by our tests. + */ +function field_permission_example_entity_test_access(EntityInterface $entity, $operation, AccountInterface $account, $langcode) { + if ($operation == 'edit') { + $perms = [ + 'administer the fieldnote field', + 'edit any fieldnote', + 'edit own fieldnote', + ]; + foreach ($perms as $perm) { + if ($account->hasPermission($perm)) { + return AccessResult::allowed(); + } + } + } + return AccessResult::neutral(); +} +/** + * @} End of "defgroup field_permission_example". + */ + +/** + * Implements hook_theme(). + * + * Since we have a lot to explain, we're going to use Twig to do it. + */ +function field_permission_example_theme() { + return [ + 'field_permission_description' => [ + 'template' => 'description', + 'variables' => [ + 'admin_link' => NULL, + ], + ], + ]; +} diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.permissions.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.permissions.yml new file mode 100644 index 000000000..c70eb2b35 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.permissions.yml @@ -0,0 +1,11 @@ +# Permissions for the field_permission_example module +'view own fieldnote': + title: View own fieldnote +'edit own fieldnote': + title: Edit own fieldnote +'view any fieldnote': + title: View any fieldnote +'edit any fieldnote': + title: Edit any fieldnote +'administer the fieldnote field': + title: Administer settings for the fieldnote field. diff --git a/sites/all/modules/examples/examples/field_permission_example/field_permission_example.routing.yml b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.routing.yml new file mode 100644 index 000000000..b58b97b47 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/field_permission_example.routing.yml @@ -0,0 +1,7 @@ +field_permission_example.description: + path: '/examples/field-permission-example' + defaults: + _title: 'Field Permission Example' + _controller: '\Drupal\field_permission_example\Controller\FieldPermissionExampleController::description' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Controller/FieldPermissionExampleController.php b/sites/all/modules/examples/examples/field_permission_example/src/Controller/FieldPermissionExampleController.php new file mode 100644 index 000000000..2cca08d23 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/src/Controller/FieldPermissionExampleController.php @@ -0,0 +1,28 @@ +t('the permissions admin page'), 'user.admin_permissions')->toString(); + + $build = [ + 'description' => [ + '#theme' => 'field_permission_description', + '#admin_link' => $permissions_admin_link, + ], + ]; + return $build; + } + +} diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php new file mode 100644 index 000000000..ce91470dd --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldFormatter/SimpleTextFormatter.php @@ -0,0 +1,49 @@ + $item) { + $elements[$delta] = [ + // We wrap the fieldnote content up in a div tag. + '#type' => 'html_tag', + '#tag' => 'div', + // This text is auto-XSS escaped. See docs for the html_tag element. + '#value' => $item->value, + // Let's give the note a nice sticky-note CSS appearance. + '#attributes' => [ + 'class' => 'stickynote', + ], + // ..And this is the CSS for the stickynote. + '#attached' => [ + 'library' => ['field_permission_example/fieldnote_sticky'], + ], + ]; + } + + return $elements; + } + +} diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php new file mode 100644 index 000000000..0793e1691 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldType/FieldNote.php @@ -0,0 +1,55 @@ + [ + 'value' => [ + 'type' => 'text', + 'size' => 'normal', + 'not null' => FALSE, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $value = $this->get('value')->getValue(); + return $value === NULL || $value === ''; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties['value'] = DataDefinition::create('string') + ->setLabel(t('Field Note')); + + return $properties; + } + +} diff --git a/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php new file mode 100644 index 000000000..cd24f05df --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/src/Plugin/Field/FieldWidget/TextWidget.php @@ -0,0 +1,35 @@ +value) ? $items[$delta]->value : ''; + $element += [ + '#type' => 'textarea', + '#default_value' => $value, + ]; + return ['value' => $element]; + } + +} diff --git a/sites/all/modules/examples/examples/field_permission_example/templates/description.html.twig b/sites/all/modules/examples/examples/field_permission_example/templates/description.html.twig new file mode 100644 index 000000000..1df2bc8dc --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/templates/description.html.twig @@ -0,0 +1,28 @@ +{# +/** + * @file + * Contains the text of the field_permission_example explanation/description page + * + * Available variables: + * - admin_link: The translated link pointing to the administer permissions page. + */ +#} + +
+{% trans %} +

The Field Permission Example module shows how you can restrict view and edit permissions + within your field implementation. It adds a new field type called Fieldnote. Fieldnotes + appear as simple text boxes on the create/edit form, and as sticky notes when viewed. + By 'sticky note' we mean 'Post-It Note' but that's a trademarked term.

+ +

To see this field in action, add it to a content type or user profile. Go to the + permissions page ({{admin_link}}) and look at the 'Field Permission Example' section. This + allows you to change which roles can see and edit Fieldnote fields.

+ +

Creating different users with different capabilities will let you see these behaviors + in action. Fieldnote helpfully displays a message telling you which permissions it is + trying to resolve for the current field/user combination.

+ +

Definitely look through the code to see various implementation details.

+{% endtrans %} +
diff --git a/sites/all/modules/examples/examples/field_permission_example/tests/src/Kernel/FieldNoteItemTest.php b/sites/all/modules/examples/examples/field_permission_example/tests/src/Kernel/FieldNoteItemTest.php new file mode 100644 index 000000000..ad932a744 --- /dev/null +++ b/sites/all/modules/examples/examples/field_permission_example/tests/src/Kernel/FieldNoteItemTest.php @@ -0,0 +1,290 @@ +container->get('entity_type.manager'); + + // Set up our entity_type and user type for our new field: + $type_manager + ->getStorage('field_storage_config') + ->create([ + 'field_name' => 'field_fieldnote', + 'entity_type' => 'entity_test', + 'type' => 'field_permission_example_fieldnote', + ])->save(); + + $type_manager + ->getStorage('field_config') + ->create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_fieldnote', + 'bundle' => 'entity_test', + ])->save(); + + // Create a form display for the default form mode, and + // add our field type. + $type_manager + ->getStorage('entity_form_display') + ->create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + ]) + ->setComponent('field_fieldnote', [ + 'type' => 'field_permission_example_widget', + ]) + ->save(); + + // Now do this for the user type. + $type_manager + ->getStorage('field_storage_config') + ->create([ + 'field_name' => 'user_fieldnote', + 'entity_type' => 'user', + 'type' => 'field_permission_example_fieldnote', + ])->save(); + + $type_manager + ->getStorage('field_config') + ->create([ + 'entity_type' => 'user', + 'field_name' => 'user_fieldnote', + 'bundle' => 'user', + ])->save(); + + // Fetch a form display for a user. This may already exist, so check as + // Core does. + // @see https://api.drupal.org/api/drupal/core%21includes%21entity.inc/function/entity_get_form_display/8 + $entity_form_display + = $type_manager + ->getStorage('entity_form_display') + ->load('user.user.default'); + if (empty($entity_form_display)) { + $entity_form_display + = $type_manager + ->getStorage('entity_form_display') + ->create([ + 'targetEntityType' => 'user', + 'bundle' => 'user', + 'mode' => 'default', + 'status' => TRUE, + ]); + } + // And add our fancy field to that display: + $entity_form_display->setComponent('field_fieldnote', [ + 'type' => 'field_permission_example_widget', + ])->save(); + + } + + /** + * Test entity fields of the field_permission_example_fieldnote field type. + */ + public function testFieldNoteItem() { + // Verify entity creation. + $type_manager = $this->container->get('entity_type.manager'); + $entity + = $type_manager + ->getStorage('entity_test') + ->create([]); + $value = 'This is an epic entity'; + $entity->field_fieldnote = $value; + $entity->name->value = $this->randomMachineName(); + $entity->save(); + + // Verify entity has been created properly. + $id = $entity->id(); + $entity + = $type_manager + ->getStorage('entity_test') + ->load($id); + + $this->assertTrue($entity->field_fieldnote instanceof FieldItemListInterface, 'Field implements interface.'); + $this->assertTrue($entity->field_fieldnote[0] instanceof FieldItemInterface, 'Field item implements interface.'); + $this->assertEqual($entity->field_fieldnote->value, $value); + $this->assertEqual($entity->field_fieldnote[0]->value, $value); + + // Verify changing the field's value. + $new_value = $this->randomMachineName(); + $entity->field_fieldnote->value = $new_value; + $this->assertEqual($entity->field_fieldnote->value, $new_value); + + // Read changed entity and assert changed values. + $entity->save(); + + $entity + = $type_manager + ->getStorage('entity_test') + ->load($id); + + $this->assertEqual($entity->field_fieldnote->value, $new_value); + + // Test sample item generation. + $entity + = $type_manager + ->getStorage('entity_test') + ->create([]); + + $entity->field_fieldnote->generateSampleItems(); + $this->entityValidateAndSave($entity); + } + + /** + * Test multiple access scenarios for the fieldnote field. + */ + public function testFieldNoteAccess() { + + // Let's set up some scenarios. + $scenarios = [ + 'admin_type' => [ + 'perms' => ['administer the fieldnote field'], + 'can_view_any' => TRUE, + 'can_edit_any' => TRUE, + 'can_view_own' => TRUE, + 'can_edit_own' => TRUE, + ], + 'low_access' => [ + 'perms' => ['view test entity'], + 'can_view_any' => FALSE, + 'can_edit_any' => FALSE, + 'can_view_own' => FALSE, + 'can_edit_own' => FALSE, + ], + 'view_any' => [ + 'perms' => [ + 'view test entity', + 'view any fieldnote', + ], + 'can_view_any' => TRUE, + 'can_edit_any' => FALSE, + 'can_view_own' => FALSE, + 'can_edit_own' => FALSE, + ], + 'edit_any' => [ + 'perms' => [ + 'view test entity', + 'view any fieldnote', + 'edit any fieldnote', + ], + 'can_view_any' => TRUE, + 'can_edit_any' => TRUE, + 'can_view_own' => FALSE, + 'can_edit_own' => FALSE, + ], + 'view_own' => [ + 'perms' => [ + 'view test entity', + 'view own fieldnote', + ], + 'can_view_any' => FALSE, + 'can_edit_any' => FALSE, + 'can_view_own' => TRUE, + 'can_edit_own' => FALSE, + ], + 'edit_own' => [ + 'perms' => [ + 'view test entity', + 'view own fieldnote', + 'edit own fieldnote', + ], + 'can_view_any' => FALSE, + 'can_edit_any' => FALSE, + 'can_view_own' => TRUE, + 'can_edit_own' => TRUE, + ], + ]; + + $value = 'This is an epic entity'; + // We also need to test users as an entity to attach to. They work + // a little differently than most content entity types: + $arbitrary_user = $this->createUser([], 'Some User'); + $arbitrary_user->user_fieldnote = $value; + $arbitrary_user->save(); + + foreach ($scenarios as $name => $scenario) { + $test_user = $this->createUser($scenario['perms'], $name); + $entity = entity_create('entity_test'); + $entity->field_fieldnote = $value; + $entity->name->value = $this->randomMachineName(); + $entity->save(); + + foreach (['can_view_any', 'can_edit_any'] as $op) { + $this->doAccessAssertion($entity, 'field_fieldnote', $test_user, $name, $op, $scenario[$op]); + $this->doAccessAssertion($arbitrary_user, 'user_fieldnote', $test_user, $name, $op, $scenario[$op]); + } + + if ($scenario['can_view_own'] or $scenario['can_edit_own']) { + $entity->user_id = $test_user; + $entity->save(); + $test_user->user_fieldnote = $value; + $test_user->save(); + + foreach (['can_view_own', 'can_edit_own'] as $op) { + $this->doAccessAssertion($entity, 'field_fieldnote', $test_user, $name, $op, $scenario[$op]); + $this->doAccessAssertion($test_user, 'user_fieldnote', $test_user, $name, $op, $scenario[$op]); + } + } + } + + } + + /** + * Helper routine to run the assertions. + */ + protected function doAccessAssertion($entity, $field_name, $account, $name, $op, $expected) { + $expect_str = $expected ? "CAN" : "CANNOT"; + $assert_str = "$name $expect_str do $op on field $field_name"; + $operation = preg_match('/edit/', $op) ? "edit" : "view"; + $result = $entity->$field_name->access($operation, $account); + if ($expected) { + $this->assertTrue($result, $assert_str); + } + else { + $this->assertFalse($result, $assert_str); + } + } + +} diff --git a/sites/all/modules/examples/examples/file_example/file_example.info.yml b/sites/all/modules/examples/examples/file_example/file_example.info.yml new file mode 100644 index 000000000..7248eb3b0 --- /dev/null +++ b/sites/all/modules/examples/examples/file_example/file_example.info.yml @@ -0,0 +1,15 @@ +name: File Example +type: module +description: Demonstrates using the Drupal File API and stream wrappers. +package: Example modules +# core: 8.x +dependencies: + - drupal:file + - examples:examples + - examples:stream_wrapper_example + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/file_example/file_example.links.menu.yml b/sites/all/modules/examples/examples/file_example/file_example.links.menu.yml new file mode 100644 index 000000000..cce6e579f --- /dev/null +++ b/sites/all/modules/examples/examples/file_example/file_example.links.menu.yml @@ -0,0 +1,7 @@ +# +# This link will appear in the "Tools" menu. +# +file_example.fileapi: + title: File Example + parent: file_example.description + route_name: file_example.fileapi diff --git a/sites/all/modules/examples/examples/file_example/file_example.module b/sites/all/modules/examples/examples/file_example/file_example.module new file mode 100644 index 000000000..ab2e9801b --- /dev/null +++ b/sites/all/modules/examples/examples/file_example/file_example.module @@ -0,0 +1,97 @@ +getAccount(); + if ($account->hasPermission($permission)) { + return [ + 'Content-Type: text/plain', + ]; + } + } +} + +/** + * @} End of "defgroup file_example". + */ diff --git a/sites/all/modules/examples/examples/file_example/file_example.permissions.yml b/sites/all/modules/examples/examples/file_example/file_example.permissions.yml new file mode 100644 index 000000000..9ab5c2a9f --- /dev/null +++ b/sites/all/modules/examples/examples/file_example/file_example.permissions.yml @@ -0,0 +1,13 @@ +'use file example': + title: Use the examples in the File Example module. + +# +# We use the following permissions in our hook_file_download implementation. +# See file_example.module for details. +# +'read private files': + title: See private files in the File Example module demo. +'read temporary files': + title: See temporary files in the File Example module demo. +'read session files': + title: See session files in the File Example module demo. diff --git a/sites/all/modules/examples/examples/file_example/file_example.routing.yml b/sites/all/modules/examples/examples/file_example/file_example.routing.yml new file mode 100644 index 000000000..ed00fad07 --- /dev/null +++ b/sites/all/modules/examples/examples/file_example/file_example.routing.yml @@ -0,0 +1,8 @@ +# Main page for our example. +file_example.fileapi: + path: '/examples/file_example' + defaults: + _form: '\Drupal\file_example\Form\FileExampleReadWriteForm' + _title: 'File Example: Use the File API to read/write a file' + requirements: + _permission: 'use file example' diff --git a/sites/all/modules/examples/examples/file_example/src/Form/FileExampleReadWriteForm.php b/sites/all/modules/examples/examples/file_example/src/Form/FileExampleReadWriteForm.php new file mode 100644 index 000000000..52a0590fa --- /dev/null +++ b/sites/all/modules/examples/examples/file_example/src/Form/FileExampleReadWriteForm.php @@ -0,0 +1,861 @@ +state = $state; + $this->fileSystem = $file_system; + $this->moduleHandler = $module_handler; + $this->requestStack = $request_stack; + $this->streamWrapperManager = $stream_wrapper_manager; + $this->sessionSchemeEnabled = $this->moduleHandler->moduleExists('stream_wrapper_example'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + $state = $container->get('state'); + $file_system = $container->get('file_system'); + $module_handler = $container->get('module_handler'); + $request_stack = $container->get('request_stack'); + $stream_wrapper_manager = $container->get('stream_wrapper_manager'); + return new static($state, $file_system, $stream_wrapper_manager, $module_handler, $request_stack); + } + + /** + * Returns a unique string identifying the form. + * + * @return string + * The unique string identifying the form. + */ + public function getFormId() { + return 'file_example_readwrite'; + } + + /** + * Get the default file. + * + * This appears in the first block of the form. + * + * @return string + * The URI of the default file. + */ + protected function getDefaultFile() { + $fall_back_value = $this->sessionSchemeEnabled ? 'session://drupal.txt' : 'public://drupal.txt'; + $default_file = $this->state->get('file_example_default_file', $fall_back_value); + return $default_file; + } + + /** + * Fetch a SessionWrapper object. + * + * This is used to change relevant attributes of the Session. This will return + * FALSE if the stream_wrapper_example is not enabled. + * + * @return Drupal\stream_wrapper_example\StreamWrapper\SessionWrapper|bool + * Wrapper object to manipulate the SESSION storage or FALSE if the session + * wrapper is unavailable. + */ + protected function getSessionWrapper() { + if ($this->sessionSchemeEnabled) { + return new SessionWrapper($this->requestStack); + } + return FALSE; + } + + /** + * Set the default file. + * + * Set a default URI of the file used for read and write operations. + * + * @param string $uri + * URI to save for future display in the form. + */ + protected function setDefaultFile($uri) { + $this->state->set('file_example_default_file', (string) $uri); + } + + /** + * Get the default directory. + * + * @return string + * The URI of the default directory. + */ + protected function getDefaultDirectory() { + $fall_back_value = $this->sessionSchemeEnabled ? 'session://directory1' : 'public://directory1'; + $default_directory = $this->state->get('file_example_default_directory', $fall_back_value); + return $default_directory; + } + + /** + * Set the default directory. + * + * @param string $uri + * URI to save for later form display. + */ + protected function setDefaultDirectory($uri) { + $this->state->set('file_example_default_directory', (string) $uri); + } + + /** + * Utility function to check for and return a managed file. + * + * In this demonstration code we don't necessarily know if a file is managed + * or not, so often need to check to do the correct behavior. Normal code + * would not have to do this, as it would be working with either managed or + * unmanaged files. + * + * @param string $uri + * The URI of the file, like public://test.txt. + * + * @return \Drupal\file\Entity\FileInterface|bool + * A file object that matches the URI, or FALSE if not a managed file. + * + * @todo This should still work. An entity query could be used instead. + * May be other alternatives. + */ + private static function getManagedFile($uri) { + $fid = Database::getConnection('default')->query( + 'SELECT fid FROM {file_managed} WHERE uri = :uri', + [':uri' => $uri] + )->fetchField(); + if (!empty($fid)) { + $file_object = File::load($fid); + return $file_object; + } + return FALSE; + } + + /** + * Prepare Url objects to prevent exceptions by the URL generator. + * + * Helper function to get us an external URL if this is legal, and to catch + * the exception Drupal throws if this is not possible. + * + * In Drupal 8, the URL generator is very sensitive to how you set things + * up, and some functions, in particular LinkGeneratorTrait::l(), will throw + * exceptions if you deviate from what's expected. This function will raise + * the chances your URL will be valid, and not do this. + * + * @param \Drupal\file\Entity\File|string $file_object + * A file entity object. + * + * @return \Drupal\Core\Url + * A Url object that can be displayed as an internal URL. + */ + protected function getExternalUrl($file_object) { + if ($file_object instanceof FileInterface) { + $uri = $file_object->getFileUri(); + } + else { + // A little tricky, since file.inc is a little inconsistent, but often + // this is a Uri. + $uri = file_create_url($file_object); + } + + try { + // If we have been given a PHP stream URI, ask the stream itself if it + // knows how to create an external URL. + $wrapper = $this->streamWrapperManager->getViaUri($uri); + if ($wrapper) { + $external_url = $wrapper->getExternalUrl(); + // Some streams may not have the concept of an external URL, so we + // check here to make sure, since the example assumes this. + if ($external_url) { + $url = Url::fromUri($external_url); + return $url; + } + } + else { + $url = Url::fromUri($uri); + // If we did not throw on ::fromUri (you can), we return the URL. + return $url; + } + } + catch (\Exception $e) { + return FALSE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $default_file = $this->getDefaultFile(); + $default_directory = $this->getDefaultDirectory(); + + $form['description'] = [ + '#markup' => $this->t('This form demonstrates the Drupal 8 file api. Experiment with the form, and then look at the submit handlers in the code to understand the file api.'), + ]; + + $form['write_file'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Write to a file'), + ]; + $form['write_file']['write_contents'] = [ + '#type' => 'textfield', + '#title' => $this->t('Enter something you would like to write to a file'), + '#default_value' => $this->t('Put some text here or just use this text'), + ]; + + $form['write_file']['destination'] = [ + '#type' => 'textfield', + '#default_value' => $default_file, + '#title' => $this->t('Optional: Enter the streamwrapper saying where it should be written'), + '#description' => $this->t('This may be public://some_dir/test_file.txt or private://another_dir/some_file.txt, for example. If you include a directory, it must already exist. The default is "public://". Since this example supports session://, you can also use something like session://somefile.txt.'), + ]; + + $form['write_file']['managed_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Write managed file'), + '#submit' => ['::handleManagedFile'], + ]; + $form['write_file']['unmanaged_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Write unmanaged file'), + '#submit' => ['::handleUnmanagedFile'], + ]; + $form['write_file']['unmanaged_php'] = [ + '#type' => 'submit', + '#value' => $this->t('Unmanaged using PHP'), + '#submit' => ['::handleUnmanagedPhp'], + ]; + + $form['fileops'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Read from a file'), + ]; + $form['fileops']['fileops_file'] = [ + '#type' => 'textfield', + '#default_value' => $default_file, + '#title' => $this->t('Enter the URI of a file'), + '#description' => $this->t('This must be a stream-type description like public://some_file.txt or http://drupal.org or private://another_file.txt or (for this example) session://yet_another_file.txt.'), + ]; + $form['fileops']['read_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Read the file and store it locally'), + '#submit' => ['::handleFileRead'], + ]; + $form['fileops']['delete_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete file'), + '#submit' => ['::handleFileDelete'], + ]; + $form['fileops']['check_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Check to see if file exists'), + '#submit' => ['::handleFileExists'], + ]; + + $form['directory'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Create or prepare a directory'), + ]; + + $form['directory']['directory_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Directory to create/prepare/delete'), + '#default_value' => $default_directory, + '#description' => $this->t('This is a directory as in public://some/directory or private://another/dir.'), + ]; + $form['directory']['create_directory'] = [ + '#type' => 'submit', + '#value' => $this->t('Create directory'), + '#submit' => ['::handleDirectoryCreate'], + ]; + $form['directory']['delete_directory'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete directory'), + '#submit' => ['::handleDirectoryDelete'], + ]; + $form['directory']['check_directory'] = [ + '#type' => 'submit', + '#value' => $this->t('Check to see if directory exists'), + '#submit' => ['::handleDirectoryExists'], + ]; + + $form['debug'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Debugging'), + ]; + // The Session Wrapper Exampple is not yet committed, so + // we hide this button until this happens. + $form['debug']['show_raw_session'] = [ + '#type' => 'submit', + '#value' => $this->t('Show raw $_SESSION contents'), + '#submit' => ['::handleShowSession'], + '#access' => $this->sessionSchemeEnabled, + ]; + $form['debug']['reset_session'] = [ + '#type' => 'submit', + '#value' => t('Reset the Session'), + '#submit' => ['::handleResetSession'], + ]; + + return $form; + } + + /** + * Submit handler to write a managed file. + * + * A "managed file" is a file that Drupal tracks as a file entity. It's the + * standard way Drupal manages files in file fields and elsewhere. + * + * The key functions used here are: + * - file_save_data(), which takes a buffer and saves it to a named file and + * also creates a tracking record in the database and returns a file object. + * In this function we use FILE_EXISTS_RENAME (the default) as the argument, + * which means that if there's an existing file, create a new non-colliding + * filename and use it. + * - file_create_url(), which converts a URI in the form public://junk.txt or + * private://something/test.txt into a URL like + * http://example.com/sites/default/files/junk.txt. + * * @param array $form + * An associative array containing the structure of the form. + * + * @param array &$form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function handleManagedFile(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $data = $form_values['write_contents']; + $uri = !empty($form_values['destination']) ? $form_values['destination'] : NULL; + + // Managed operations work with a file object. + $file_object = \file_save_data($data, $uri, FILE_EXISTS_RENAME); + if (!empty($file_object)) { + $url = $this->getExternalUrl($file_object); + $this->setDefaultFile($file_object->getFileUri()); + $file_data = $file_object->toArray(); + if ($url) { + drupal_set_message( + $this->t('Saved managed file: %file to destination %destination (accessible via this URL, actual uri=@uri)', + [ + '%file' => print_r($file_data, TRUE), + '%destination' => $uri, + '@uri' => $file_object->getFileUri(), + ':url' => $url->toString(), + ] + ) + ); + } + else { + // This Uri is not routable, so we cannot give a link to it. + drupal_set_message( + $this->t('Saved managed file: %file to destination %destination (no URL, since this stream type does not support it)', + [ + '%file' => print_r($file_data, TRUE), + '%destination' => $uri, + '@uri' => $file_object->getFileUri(), + ] + ) + ); + + } + } + else { + drupal_set_message(t('Failed to save the managed file'), 'error'); + } + + } + + /** + * Submit handler to write an unmanaged file. + * + * An unmanaged file is a file that Drupal does not track. A standard + * operating system file, in other words. + * + * The key functions used here are: + * - file_unmanaged_save_data(), which takes a buffer and saves it to a named + * file, but does not create any kind of tracking record in the database. + * This example uses FILE_EXISTS_REPLACE for the third argument, meaning + * that if there's an existing file at this location, it should be replaced. + * - file_create_url(), which converts a URI in the form public://junk.txt or + * private://something/test.txt into a URL like + * http://example.com/sites/default/files/junk.txt. + * * @param array $form + * An associative array containing the structure of the form. + * + * @param array &$form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function handleUnmanagedFile(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $data = $form_values['write_contents']; + $destination = !empty($form_values['destination']) ? $form_values['destination'] : NULL; + + // With the unmanaged file we just get a filename back. + $filename = file_unmanaged_save_data($data, $destination, FILE_EXISTS_REPLACE); + if ($filename) { + $url = $this->getExternalUrl($filename); + $this->setDefaultFile($filename); + if ($url) { + drupal_set_message( + $this->t('Saved file as %filename (accessible via this URL, uri=@uri)', + [ + '%filename' => $filename, + '@uri' => $filename, + ':url' => $url->toString(), + ] + ) + ); + } + else { + drupal_set_message( + $this->t('Saved file as %filename (not accessible externally)', + [ + '%filename' => $filename, + '@uri' => $filename, + ] + ) + ); + } + } + else { + drupal_set_message(t('Failed to save the file'), 'error'); + } + } + + /** + * Submit handler to write an unmanaged file using plain PHP functions. + * + * The key functions used here are: + * - file_unmanaged_save_data(), which takes a buffer and saves it to a named + * file, but does not create any kind of tracking record in the database. + * - file_create_url(), which converts a URI in the form public://junk.txt or + * private://something/test.txt into a URL like + * http://example.com/sites/default/files/junk.txt. + * - drupal_tempnam() generates a temporary filename for use. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function handleUnmanagedPhp(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $data = $form_values['write_contents']; + $destination = !empty($form_values['destination']) ? $form_values['destination'] : NULL; + + if (empty($destination)) { + // If no destination has been provided, use a generated name. + $destination = $this->fileSystem->tempnam('public://', 'file'); + } + + // With all traditional PHP functions we can use the stream wrapper notation + // for a file as well. + $fp = fopen($destination, 'w'); + + // To demonstrate the fact that everything is based on streams, we'll do + // multiple 5-character writes to put this to the file. We could easily + // (and far more conveniently) write it in a single statement with + // fwrite($fp, $data). + $length = strlen($data); + $write_size = 5; + for ($i = 0; $i < $length; $i += $write_size) { + $result = fwrite($fp, substr($data, $i, $write_size)); + if ($result === FALSE) { + drupal_set_message(t('Failed writing to the file %file', ['%file' => $destination]), 'error'); + fclose($fp); + return; + } + } + $url = $this->getExternalUrl($destination); + $this->setDefaultFile($destination); + if ($url) { + drupal_set_message( + $this->t('Saved file as %filename (accessible via this URL, uri=@uri)', + [ + '%filename' => $destination, + '@uri' => $destination, + ':url' => $url->toString(), + ] + ) + ); + } + else { + drupal_set_message( + $this->t('Saved file as %filename (not accessible externally)', + [ + '%filename' => $destination, + '@uri' => $destination, + ] + ) + ); + } + + } + + /** + * Submit handler for reading a stream wrapper. + * + * Drupal now has full support for PHP's stream wrappers, which means that + * instead of the traditional use of all the file functions + * ($fp = fopen("/tmp/some_file.txt");) far more sophisticated and generalized + * (and extensible) things can be opened as if they were files. Drupal itself + * provides the public:// and private:// schemes for handling public and + * private files. PHP provides file:// (the default) and http://, so that a + * URL can be read or written (as in a POST) as if it were a file. In + * addition, new schemes can be provided for custom applications. The Stream + * Wrapper Example, if installed, impleents a custom 'session' scheme that + * you can test with this example. + * + * Here we take the stream wrapper provided in the form. We grab the + * contents with file_get_contents(). Notice that's it's as simple as that: + * file_get_contents("http://example.com") or + * file_get_contents("public://somefile.txt") just works. Although it's + * not necessary, we use file_unmanaged_save_data() to save this file locally + * and then find a local URL for it by using file_create_url(). + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function handleFileRead(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $uri = $form_values['fileops_file']; + + if (empty($uri) or !is_file($uri)) { + drupal_set_message(t('The file "%uri" does not exist', ['%uri' => $uri]), 'error'); + return; + } + + // Make a working filename to save this by stripping off the (possible) + // file portion of the streamwrapper. If it's an evil file extension, + // file_munge_filename() will neuter it. + $filename = file_munge_filename(preg_replace('@^.*/@', '', $uri), '', TRUE); + $buffer = file_get_contents($uri); + + if ($buffer) { + $sourcename = file_unmanaged_save_data($buffer, 'public://' . $filename); + if ($sourcename) { + $url = $this->getExternalUrl($sourcename); + $this->setDefaultFile($sourcename); + if ($url) { + drupal_set_message( + $this->t('The file was read and copied to %filename which is accessible at this URL', + [ + '%filename' => $sourcename, + ':url' => $url->toString(), + ] + ) + ); + } + else { + drupal_set_message( + $this->t('The file was read and copied to %filename (not accessible externally)', + [ + '%filename' => $sourcename, + ] + ) + ); + + } + } + else { + drupal_set_message(t('Failed to save the file')); + } + } + else { + // We failed to get the contents of the requested file. + drupal_set_message(t('Failed to retrieve the file %file', ['%file' => $uri])); + } + + } + + /** + * Submit handler to delete a file. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function handleFileDelete(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $uri = $form_values['fileops_file']; + + // Since we don't know if the file is managed or not, look in the database + // to see. Normally, code would be working with either managed or unmanaged + // files, so this is not a typical situation. + $file_object = self::getManagedFile($uri); + + // If a managed file, use file_delete(). + if (!empty($file_object)) { + // While file_delete should return FALSE on failure, + // it can currently throw an exception on certain cache states. + try { + // This no longer returns a result code. If things go bad, + // it will throw an exception: + file_delete($file_object->id()); + drupal_set_message(t('Successfully deleted managed file %uri', ['%uri' => $uri])); + $this->setDefaultFile($uri); + } + catch (\Exception $e) { + drupal_set_message(t('Failed deleting managed file %uri. Result was %result', + [ + '%uri' => $uri, + '%result' => print_r($e->getMessage(), TRUE), + ] + ), 'error'); + } + } + // Else use file_unmanaged_delete(). + else { + $result = file_unmanaged_delete($uri); + if ($result !== TRUE) { + drupal_set_message(t('Failed deleting unmanaged file %uri', ['%uri' => $uri, 'error'])); + } + else { + drupal_set_message(t('Successfully deleted unmanaged file %uri', ['%uri' => $uri])); + $this->setDefaultFile('file_example_default_file', $uri); + } + } + } + + /** + * Submit handler to check existence of a file. + */ + public function handleFileExists(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $uri = $form_values['fileops_file']; + if (is_file($uri)) { + drupal_set_message(t('The file %uri exists.', ['%uri' => $uri])); + } + else { + drupal_set_message(t('The file %uri does not exist.', ['%uri' => $uri])); + } + } + + /** + * Submit handler for directory creation. + * + * Here we create a directory and set proper permissions on it using + * file_prepare_directory(). + */ + public function handleDirectoryCreate(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $directory = $form_values['directory_name']; + + // The options passed to file_prepare_directory are a bitmask, so we can + // specify either FILE_MODIFY_PERMISSIONS (set permissions on the + // directory), FILE_CREATE_DIRECTORY, or both together: + // FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY. + // FILE_MODIFY_PERMISSIONS will set the permissions of the directory by + // by default to 0755, or to the value of the variable + // 'file_chmod_directory'. + if (!file_prepare_directory($directory, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY)) { + drupal_set_message(t('Failed to create %directory.', ['%directory' => $directory]), 'error'); + } + else { + $result = is_dir($directory); + drupal_set_message(t('Directory %directory is ready for use.', ['%directory' => $directory])); + $this->setDefaultDirectory($directory); + } + } + + /** + * Submit handler for directory deletion. + * + * @see file_unmanaged_delete_recursive() + */ + public function handleDirectoryDelete(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $directory = $form_values['directory_name']; + + $result = file_unmanaged_delete_recursive($directory); + if (!$result) { + drupal_set_message(t('Failed to delete %directory.', ['%directory' => $directory]), 'error'); + } + else { + drupal_set_message(t('Recursively deleted directory %directory.', ['%directory' => $directory])); + $this->setDefaultDirectory($directory); + } + } + + /** + * Submit handler to test directory existence. + * + * This actually just checks to see if the directory is writable. + * + * @param array $form + * FormAPI form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * FormAPI form state. + */ + public function handleDirectoryExists(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + $directory = $form_values['directory_name']; + $result = is_dir($directory); + if (!$result) { + drupal_set_message(t('Directory %directory does not exist.', ['%directory' => $directory])); + } + else { + drupal_set_message(t('Directory %directory exists.', ['%directory' => $directory])); + } + } + + /** + * Utility submit function to show the contents of $_SESSION. + */ + public function handleShowSession(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + // If the devel module is installed, use it's nicer message format. + if ($this->moduleHandler->moduleExists('devel')) { + // @codingStandardsIgnoreStart + // We wrap this in the coding standards ignore tags because the use of + // function dsm() is discouraged. + dsm($this->getStoredData(), $this->t('Entire $_SESSION["file_example"]')); + // @codingStandardsIgnoreEnd + } + else { + drupal_set_message('
' . print_r($this->getStoredData(), TRUE) . '
'); + } + } + + /** + * Utility submit function to reset the demo. + * + * @param array $form + * FormAPI form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * FormAPI form state. + * + * @todo Note this does NOT clear any managed file references in Drupal's DB. + * It might be a good idea to add this. + */ + public function handleResetSession(array &$form, FormStateInterface $form_state) { + $this->state->delete('file_example_default_file'); + $this->state->delete('file_example_default_directory'); + $this->clearStoredData(); + drupal_set_message('Session reset.'); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // We don't use this, but the interface requires us to implement it. + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // We don't use this, but the interface requires us to implement it. + } + + /** + * Get our stored data for display. + */ + protected function getStoredData() { + $handle = $this->getSessionWrapper(); + if ($handle) { + return $handle->getPath(''); + } + return "SESSION STORE IS NOT ENABLED"; + } + + /** + * Reset our stored data. + */ + protected function clearStoredData() { + $handle = $this->getSessionWrapper(); + if ($handle) { + return $handle->cleanUpStore(); + } + } + +} diff --git a/sites/all/modules/examples/examples/file_example/tests/src/Functional/FileExampleTest.php b/sites/all/modules/examples/examples/file_example/tests/src/Functional/FileExampleTest.php new file mode 100644 index 000000000..0383193c9 --- /dev/null +++ b/sites/all/modules/examples/examples/file_example/tests/src/Functional/FileExampleTest.php @@ -0,0 +1,152 @@ +assertSession(); + + // Our test user needs to access some non-standard file types, + // so we bless it accordingly. + $permissions = [ + 'use file example', + 'read private files', + 'read temporary files', + 'read session files', + ]; + $priviledged_user = $this->drupalCreateUser($permissions); + $this->drupalLogin($priviledged_user); + + $expected_text = [ + 'Write managed file' => 'Saved managed file', + 'Write unmanaged file' => 'Saved file as', + 'Unmanaged using PHP' => 'Saved file as', + ]; + // For each of the three buttons == three write types. + $buttons = [ + 'Write managed file', + 'Write unmanaged file', + 'Unmanaged using PHP', + ]; + foreach ($buttons as $button) { + // For each scheme supported by Drupal + the session:// wrapper, + // which is defined in the stream_wrapper_exampnle. + $schemes = ['public', 'private', 'temporary', 'session']; + foreach ($schemes as $scheme) { + // Create a directory for use. + $dirname = $scheme . '://' . $this->randomMachineName(10); + + // Directory does not yet exist; assert that. + $edit = [ + 'directory_name' => $dirname, + ]; + $this->drupalPostForm('examples/file_example', $edit, 'Check to see if directory exists'); + $assert->pageTextContains(t('Directory @dirname does not exist', ['@dirname' => $dirname])); + + $this->drupalPostForm('examples/file_example', $edit, 'Create directory'); + $assert->pageTextContains(t('Directory @dirname is ready for use', ['@dirname' => $dirname])); + + $this->drupalPostForm('examples/file_example', $edit, 'Check to see if directory exists'); + $assert->pageTextContains(t('Directory @dirname exists', ['@dirname' => $dirname])); + + // Create a file in the directory we created. + $content = $this->randomMachineName(30); + $filename = $dirname . '/' . $this->randomMachineName(30) . '.txt'; + + // Assert that the file we're about to create does not yet exist. + $edit = [ + 'fileops_file' => $filename, + ]; + $this->drupalPostForm('examples/file_example', $edit, 'Check to see if file exists'); + $assert->pageTextContains(t('The file @filename does not exist', ['@filename' => $filename])); + + $this->verbose("Processing button=$button, scheme=$scheme, dir=$dirname, file=$filename"); + $edit = [ + 'write_contents' => $content, + 'destination' => $filename, + ]; + $this->drupalPostForm('examples/file_example', $edit, $button); + $this->verbose($expected_text[$button], "Button Text"); + $assert->pageTextContains($expected_text[$button]); + + // Capture the name of the output file, as it might have changed due + // to file renaming. + $element = $this->xpath('//span[@id="uri"]'); + $output_filename = (string) $element[0]->getText(); + $this->verbose($output_filename, 'Name of output file'); + + // Click the link provided that is an easy way to get the data for + // checking and make sure that the data we put in is what we get out. + if (!in_array($scheme, [])) { + $this->clickLink(t('this URL')); + $assert->statusCodeEquals(200); + // assertText give sketchy answers when the content is *exactly* the + // contents of the buffer, so let's do something less fragile. + // $this->assertText($content); + $buffer = $this->getSession()->getPage()->getContent(); + $this->assertEquals($content, $buffer); + } + + // Verify that the file exists. + $edit = [ + 'fileops_file' => $filename, + ]; + $this->drupalPostForm('examples/file_example', $edit, 'Check to see if file exists'); + $assert->pageTextContains("The file $filename exists"); + + // Now read the file that got written above and verify that we can use + // the writing tools. + $edit = [ + 'fileops_file' => $output_filename, + ]; + $this->drupalPostForm('examples/file_example', $edit, 'Read the file and store it locally'); + + $assert->pageTextContains('The file was read and copied'); + + $edit = [ + 'fileops_file' => $filename, + ]; + + $this->drupalPostForm('examples/file_example', $edit, 'Delete file'); + $assert->pageTextContains('Successfully deleted'); + $this->drupalPostForm('examples/file_example', $edit, 'Check to see if file exists'); + $assert->pageTextContains(t('The file @filename does not exist', ['@filename' => $filename])); + + $edit = [ + 'directory_name' => $dirname, + ]; + $this->drupalPostForm('examples/file_example', $edit, 'Delete directory'); + $this->drupalPostForm('examples/file_example', $edit, 'Check to see if directory exists'); + $assert->pageTextContains(t('Directory @dirname does not exist', ['@dirname' => $dirname])); + } + } + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.info.yml b/sites/all/modules/examples/examples/form_api_example/form_api_example.info.yml new file mode 100644 index 000000000..f43aa205a --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.info.yml @@ -0,0 +1,15 @@ +name: Form API Example +type: module +description: Demonstrates the Drupal Form API. +package: Example modules +# core: 8.x +configure: form_api_example.description +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.links.menu.yml b/sites/all/modules/examples/examples/form_api_example/form_api_example.links.menu.yml new file mode 100644 index 000000000..7be1d7569 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.links.menu.yml @@ -0,0 +1,78 @@ +# Define default links for this module. +form_api_example.description: + title: Form API Examples + description: Form examples using Drupal Form API. + route_name: form_api_example.description + expanded: TRUE + +form_api_example.simple_form: + title: Simple form + description: A simple form example with submit processing. + route_name: form_api_example.simple_form + parent: form_api_example.description + weight: -9 + +form_api_example.multistep_form: + title: Multistep form + description: Multistep form example with submit processing. + route_name: form_api_example.multistep_form + parent: form_api_example.description + weight: -8 + +form_api_example.input_demo: + title: Common input elements + description: A form to demonstrate input elements. + route_name: form_api_example.input_demo + parent: form_api_example.description + weight: -7 + +form_api_example.build_demo: + title: Build form demo + description: Demonstrates the order of firing of from controller methods. + route_name: form_api_example.build_demo + parent: form_api_example.description + weight: -6 + +form_api_example.container_demo: + title: Container elements + description: A form to demonstrate use of containers. + route_name: form_api_example.container_demo + parent: form_api_example.description + weight: -5 + +form_api_example.state_demo: + title: Form state binding + description: A form to demonstrate binding of form state. + route_name: form_api_example.state_demo + parent: form_api_example.description + weight: -4 + +form_api_example.vertical_tabs_demo: + title: Vertical tab elements + description: A form to demonstrate use of vertical tabs. + route_name: form_api_example.vertical_tabs_demo + parent: form_api_example.description + weight: -3 + +form_api_example.ajax_color_demo: + title: Ajax form + description: AJAX form demo. + route_name: form_api_example.ajax_color_demo + parent: form_api_example.description + weight: -2 + +form_api_example.ajax_addmore: + title: Add-more button + description: Form with 'add more' and 'remove' buttons. + route_name: form_api_example.ajax_addmore + parent: form_api_example.description + weight: -1 + +form_api_example.modal_form: + title: Modal form + description: Demonstrates using Drupal's modal form. + route_name: form_api_example.modal_form + route_parameters: + nojs: nojs + parent: form_api_example.description + weight: 0 diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.module b/sites/all/modules/examples/examples/form_api_example/form_api_example.module new file mode 100644 index 000000000..14f421041 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.module @@ -0,0 +1,43 @@ +getForm() + * - form_api_example.routing.yml + * + * In addition to the simple example described above this module provides + * examples that demonstrate: + * - Elements used for data input + * -\Drupal\form_api_example\Form\InputDemo + * - The order of execution of form controller methods + * - \Drupal\form_api_example\Form\BuildDemo + * - Container elements used to group items + * - \Drupal\form_api_example\Form\ContainerDemo + * - Hiding elements based on the state of other elements + * - \Drupal\form_api_example\Form\StateDemo + * - Vertical Tabs elements in forms + * - \Drupal\form_api_example\Form\VerticalTabsDemo + * - Populating a portion on a form with Ajax callbacks + * - \Drupal\form_api_example\Form\AjaxColorForm + * - Adding additional fields to a form with Ajax callbacks + * - \Drupal\form_api_example\Form\AjaxAddMore + * - Modal form creation + * - \Drupal\form_api_example\Controller\Page + * - \Drupal\form_api_example\Form\ModalForm + * + * @} End of "defgroup field_example". + */ diff --git a/sites/all/modules/examples/examples/form_api_example/form_api_example.routing.yml b/sites/all/modules/examples/examples/form_api_example/form_api_example.routing.yml new file mode 100644 index 000000000..7cf20e348 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/form_api_example.routing.yml @@ -0,0 +1,100 @@ +# This routing.yml file makes both the fapi example description page and the +# included sample forms available at specific URL's on your site. A route +# maps a URL path to a controller. For page controllers it defines the +# function or method that will be called when the page is accessed. For form +# controllers the content is determined by the buildForm method defined by the +# form controller implementation. + +# Access to these paths is restricted to users with the permission +# 'access content'. This is notated as _permission: 'access content'. + +# Menu items corresponding to these URLs are defined separately in the +# form_api_example.links.menu.yml file. +form_api_example.description: + path: 'examples/form-api-example' + defaults: + _controller: '\Drupal\form_api_example\Controller\Page::description' + _title: 'Form API Examples' + requirements: + _permission: 'access content' + +form_api_example.simple_form: + path: 'examples/form-api-example/simple-form' + defaults: + _form: '\Drupal\form_api_example\Form\SimpleForm' + _title: 'Simple form' + requirements: + _permission: 'access content' + +form_api_example.input_demo: + path: 'examples/form-api-example/input-demo' + defaults: + _form: '\Drupal\form_api_example\Form\InputDemo' + _title: 'Common input elements' + requirements: + _permission: 'access content' + +form_api_example.state_demo: + path: 'examples/form-api-example/state-demo' + defaults: + _form: '\Drupal\form_api_example\Form\StateDemo' + _title: 'Form state binding' + requirements: + _permission: 'access content' + +form_api_example.container_demo: + path: 'examples/form-api-example/container-demo' + defaults: + _form: '\Drupal\form_api_example\Form\ContainerDemo' + _title: 'Container elements' + requirements: + _permission: 'access content' + +form_api_example.vertical_tabs_demo: + path: 'examples/form-api-example/vertical-tabs-demo' + defaults: + _form: '\Drupal\form_api_example\Form\VerticalTabsDemo' + _title: 'Vertical tab elements' + requirements: + _permission: 'access content' + +form_api_example.modal_form: + path: 'examples/form-api-example/modal-form/{nojs}' + defaults: + _form: '\Drupal\form_api_example\Form\ModalForm' + _title: 'Modal form' + nojs: ajax + requirements: + _permission: 'access content' + +form_api_example.ajax_color_demo: + path: 'examples/form-api-example/ajax-color-demo' + defaults: + _form: '\Drupal\form_api_example\Form\AjaxColorForm' + _title: 'AJAX color form' + requirements: + _permission: 'access content' + +form_api_example.build_demo: + path: 'examples/form-api-example/build-demo' + defaults: + _form: '\Drupal\form_api_example\Form\BuildDemo' + _title: 'Build form demo' + requirements: + _permission: 'access content' + +form_api_example.ajax_addmore: + path: 'examples/form-api-example/ajax-addmore' + defaults: + _form: '\Drupal\form_api_example\Form\AjaxAddMore' + _title: 'Add-more button' + requirements: + _permission: 'access content' + +form_api_example.multistep_form: + path: 'examples/form-api-example/multistep-form' + defaults: + _form: '\Drupal\form_api_example\Form\MultistepForm' + _title: 'Multistep form' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/form_api_example/src/Controller/Page.php b/sites/all/modules/examples/examples/form_api_example/src/Controller/Page.php new file mode 100644 index 000000000..c7b24a947 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Controller/Page.php @@ -0,0 +1,21 @@ + 'item', + '#markup' => $this->t('This example shows an add-more and a remove-last button.'), + ]; + + // Gather the number of names in the form already. + $num_names = $form_state->get('num_names'); + // We have to ensure that there is at least one name field. + if ($num_names === NULL) { + $name_field = $form_state->set('num_names', 1); + $num_names = 1; + } + + $form['#tree'] = TRUE; + $form['names_fieldset'] = [ + '#type' => 'fieldset', + '#title' => $this->t('People coming to picnic'), + '#prefix' => '
', + '#suffix' => '
', + ]; + + for ($i = 0; $i < $num_names; $i++) { + $form['names_fieldset']['name'][$i] = [ + '#type' => 'textfield', + '#title' => t('Name'), + ]; + } + + $form['names_fieldset']['actions'] = [ + '#type' => 'actions', + ]; + $form['names_fieldset']['actions']['add_name'] = [ + '#type' => 'submit', + '#value' => t('Add one more'), + '#submit' => ['::addOne'], + '#ajax' => [ + 'callback' => '::addmoreCallback', + 'wrapper' => 'names-fieldset-wrapper', + ], + ]; + // If there is more than one name, add the remove button. + if ($num_names > 1) { + $form['names_fieldset']['actions']['remove_name'] = [ + '#type' => 'submit', + '#value' => t('Remove one'), + '#submit' => ['::removeCallback'], + '#ajax' => [ + 'callback' => '::addmoreCallback', + 'wrapper' => 'names-fieldset-wrapper', + ], + ]; + } + $form_state->setCached(FALSE); + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'form_api_example_ajax_addmore'; + } + + /** + * Callback for both ajax-enabled buttons. + * + * Selects and returns the fieldset with the names in it. + */ + public function addmoreCallback(array &$form, FormStateInterface $form_state) { + $name_field = $form_state->get('num_names'); + return $form['names_fieldset']; + } + + /** + * Submit handler for the "add-one-more" button. + * + * Increments the max counter and causes a rebuild. + */ + public function addOne(array &$form, FormStateInterface $form_state) { + $name_field = $form_state->get('num_names'); + $add_button = $name_field + 1; + $form_state->set('num_names', $add_button); + $form_state->setRebuild(); + } + + /** + * Submit handler for the "remove one" button. + * + * Decrements the max counter and causes a form rebuild. + */ + public function removeCallback(array &$form, FormStateInterface $form_state) { + $name_field = $form_state->get('num_names'); + if ($name_field > 1) { + $remove_button = $name_field - 1; + $form_state->set('num_names', $remove_button); + } + $form_state->setRebuild(); + } + + /** + * Final submit handler. + * + * Reports what values were finally set. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValue(['names_fieldset', 'name']); + + $output = t('These people are coming to the picnic: @names', [ + '@names' => implode(', ', $values), + ] + ); + drupal_set_message($output); + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/AjaxColorForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/AjaxColorForm.php new file mode 100644 index 000000000..5861a14ee --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/AjaxColorForm.php @@ -0,0 +1,147 @@ + 'item', + '#markup' => $this->t('This form example demonstrates functioning of an AJAX callback.'), + ]; + + // The #ajax attribute used in the temperature input element defines an ajax + // callback that will invoke the 'updateColor' method on this form object. + // Whenever the temperature element changes, it will invoke this callback + // and replace the contents of the 'color_wrapper' container with the + // results of this method call. + $form['temperature'] = [ + '#title' => $this->t('Temperature'), + '#type' => 'select', + '#options' => $this->getColorTemperatures(), + '#empty_option' => $this->t('- Select a color temperature -'), + '#ajax' => [ + // Could also use [get_class($this), 'updateColor']. + 'callback' => '::updateColor', + 'wrapper' => 'color-wrapper', + ], + ]; + + // Add a wrapper that can be replaced with new HTML by the ajax callback. + // This is given the ID that was passed to the ajax callback in the '#ajax' + // element above. + $form['color_wrapper'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'color-wrapper'], + ]; + + // Add a color element to the color_wrapper container using the value + // from temperature to determine which colors to include in the select + // element. + $temperature = $form_state->getValue('temperature'); + if (!empty($temperature)) { + $form['color_wrapper']['color'] = [ + '#type' => 'select', + '#title' => $this->t('Color'), + '#options' => $this->getColorsByTemperature($temperature), + ]; + } + + // Add a submit button that handles the submission of the form. + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ], + ]; + + return $form; + } + + /** + * Ajax callback for the color dropdown. + */ + public function updateColor(array $form, FormStateInterface $form_state) { + return $form['color_wrapper']; + } + + /** + * Returns colors that correspond with the given temperature. + * + * @param string $temperature + * The color temperature for which to return a list of colors. Can be either + * 'warm' or 'cool'. + * + * @return array + * An associative array of colors that correspond to the given color + * temperature, suitable to use as form options. + */ + protected function getColorsByTemperature($temperature) { + return $this->getColors()[$temperature]['colors']; + } + + /** + * Returns a list of color temperatures. + * + * @return array + * An associative array of color temperatures, suitable to use as form + * options. + */ + protected function getColorTemperatures() { + return array_map(function ($color_data) { + return $color_data['name']; + }, $this->getColors()); + } + + /** + * Returns an array of colors grouped by color temperature. + * + * @return array + * An associative array of color data, keyed by color temperature. + */ + protected function getColors() { + return [ + 'warm' => [ + 'name' => $this->t('Warm'), + 'colors' => [ + 'red' => $this->t('Red'), + 'orange' => $this->t('Orange'), + 'yellow' => $this->t('Yellow'), + ], + ], + 'cool' => [ + 'name' => $this->t('Cool'), + 'colors' => [ + 'blue' => $this->t('Blue'), + 'purple' => $this->t('Purple'), + 'green' => $this->t('Green'), + ], + ], + ]; + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/BuildDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/BuildDemo.php new file mode 100644 index 000000000..5dfe16fe4 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/BuildDemo.php @@ -0,0 +1,158 @@ +displayMethodInvocation('__construct'); + } + + /** + * Update form processing information. + * + * Display the method being called and it's sequence in the form + * processing. + * + * @param string $method_name + * The method being invoked. + */ + private function displayMethodInvocation($method_name) { + self::$sequenceCounter++; + drupal_set_message(self::$sequenceCounter . ". $method_name"); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $form['description'] = [ + '#type' => 'item', + '#markup' => $this->t('Demonstrates how submit, rebuild, form-rebuild and #ajax submit work.'), + ]; + + // Simple checkbox for ajax orders. + $form['change'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Change Me'), + '#ajax' => [ + 'callback' => '::ajaxSubmit', + 'wrapper' => 'message-wrapper', + ], + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + + // Add a submit button that handles the submission of the form. + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => 'Submit', + ]; + + // Add button handlers. + $form['actions']['button'] = [ + '#type' => 'button', + '#value' => 'Rebuild', + ]; + + $form['actions']['rebuild'] = [ + '#type' => 'submit', + '#value' => 'Submit Rebuild', + '#submit' => ['::rebuildFormSubmit'], + ]; + + $form['actions']['ajaxsubmit'] = [ + '#type' => 'submit', + '#value' => 'Ajax Submit', + '#ajax' => [ + 'callback' => '::ajaxSubmit', + 'wrapper' => 'message-wrapper', + ], + ]; + + $form['messages'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'message-wrapper'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + $this->displayMethodInvocation('getFormId'); + return 'form_api_example_simple_form'; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $this->displayMethodInvocation('validateForm'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->displayMethodInvocation('submitForm'); + } + + /** + * Implements ajax submit callback. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Current state of the form. + */ + public function ajaxSubmit(array &$form, FormStateInterface $form_state) { + $this->displayMethodInvocation('ajaxSubmit'); + $form['messages']['status'] = [ + '#type' => 'status_messages', + ]; + + return $form['messages']; + } + + /** + * Implements submit callback for Rebuild button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Current state of the form. + */ + public function rebuildFormSubmit(array &$form, FormStateInterface $form_state) { + $this->displayMethodInvocation('rebuildFormSubmit'); + $form_state->setRebuild(TRUE); + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/ContainerDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/ContainerDemo.php new file mode 100644 index 000000000..503351916 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/ContainerDemo.php @@ -0,0 +1,95 @@ + 'item', + '#markup' => $this->t('This form example demonstrates container elements: details, fieldset and container.'), + ]; + + // Details containers replace D7's collapsible field sets. + $form['author'] = [ + '#type' => 'details', + '#title' => 'Author Info (type = details)', + ]; + + $form['author']['name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Name'), + ]; + + $form['author']['pen_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Pen Name'), + ]; + + // Conventional field set. + $form['book'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Book Info (type = fieldset)'), + ]; + + $form['book']['title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Title'), + ]; + + $form['book']['publisher'] = [ + '#type' => 'textfield', + '#title' => $this->t('Publisher'), + ]; + + // Containers have no visual display but wrap any contained elements in a + // div tag. + $form['accommodation'] = [ + '#type' => 'container', + ]; + + $form['accommodation']['title'] = [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->t('Special Accommodations (type = container)'), + ]; + + $form['accommodation']['diet'] = [ + '#type' => 'textfield', + '#title' => $this->t('Dietary Restrictions'), + ]; + + $form['actions'] = ['#type' => 'actions']; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'form_api_example_container_demo'; + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/DemoBase.php b/sites/all/modules/examples/examples/form_api_example/src/Form/DemoBase.php new file mode 100644 index 000000000..6c2926541 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/DemoBase.php @@ -0,0 +1,46 @@ +getValues(); + foreach ($values as $key => $value) { + $label = isset($form[$key]['#title']) ? $form[$key]['#title'] : $key; + + // Many arrays return 0 for unselected values so lets filter that out. + if (is_array($value)) { + $value = array_filter($value); + } + // Only display for controls that have titles and values. + if ($value) { + $display_value = is_array($value) ? print_r($value, 1) : $value; + $message = $this->t('Value for %title: %value', ['%title' => $label, '%value' => $display_value]); + drupal_set_message($message); + } + } + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/InputDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/InputDemo.php new file mode 100644 index 000000000..b841ac356 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/InputDemo.php @@ -0,0 +1,325 @@ + 'item', + '#markup' => $this->t('This example shows the use of all input-types.'), + ]; + + // CheckBoxes. + $form['tests_taken'] = [ + '#type' => 'checkboxes', + '#options' => ['SAT' => t('SAT'), 'ACT' => t('ACT')], + '#title' => $this->t('What standardized tests did you take?'), + '#description' => 'Checkboxes, #type = checkboxes', + ]; + + // Color. + $form['color'] = [ + '#type' => 'color', + '#title' => $this->t('Color'), + '#default_value' => '#ffffff', + '#description' => 'Color, #type = color', + ]; + + // Date. + $form['expiration'] = [ + '#type' => 'date', + '#title' => $this->t('Content expiration'), + '#default_value' => ['year' => 2020, 'month' => 2, 'day' => 15], + '#description' => 'Date, #type = date', + ]; + + // Date-time. + $form['datetime'] = [ + '#type' => 'datetime', + '#title' => 'Date Time', + '#date_increment' => 1, + '#date_timezone' => drupal_get_user_timezone(), + '#default_value' => drupal_get_user_timezone(), + '#description' => $this->t('Date time, #type = datetime'), + ]; + + // URL. + $form['url'] = [ + '#type' => 'url', + '#title' => $this->t('URL'), + '#maxlength' => 255, + '#size' => 30, + '#description' => $this->t('URL, #type = url'), + ]; + + // Email. + $form['email'] = [ + '#type' => 'email', + '#title' => $this->t('Email'), + '#description' => $this->t('Email, #type = email'), + ]; + + // Number. + $form['quantity'] = [ + '#type' => 'number', + '#title' => t('Quantity'), + '#description' => $this->t('Number, #type = number'), + ]; + + // Password. + $form['password'] = [ + '#type' => 'password', + '#title' => $this->t('Password'), + '#description' => 'Password, #type = password', + ]; + + // Password Confirm. + $form['password_confirm'] = [ + '#type' => 'password_confirm', + '#title' => $this->t('New Password'), + '#description' => $this->t('PasswordConfirm, #type = password_confirm'), + ]; + + // Range. + $form['size'] = [ + '#type' => 'range', + '#title' => t('Size'), + '#min' => 10, + '#max' => 100, + '#description' => $this->t('Range, #type = range'), + ]; + + // Radios. + $form['settings']['active'] = [ + '#type' => 'radios', + '#title' => t('Poll status'), + '#options' => [0 => $this->t('Closed'), 1 => $this->t('Active')], + '#description' => $this->t('Radios, #type = radios'), + ]; + + // Search. + $form['search'] = [ + '#type' => 'search', + '#title' => $this->t('Search'), + '#description' => $this->t('Search, #type = search'), + ]; + + // Select. + $form['favorite'] = [ + '#type' => 'select', + '#title' => $this->t('Favorite color'), + '#options' => [ + 'red' => $this->t('Red'), + 'blue' => $this->t('Blue'), + 'green' => $this->t('Green'), + ], + '#empty_option' => $this->t('-select-'), + '#description' => $this->t('Select, #type = select'), + ]; + + // Multiple values option elements. + $form['select_multiple'] = [ + '#type' => 'select', + '#title' => 'Select (multiple)', + '#multiple' => TRUE, + '#options' => [ + 'sat' => 'SAT', + 'act' => 'ACT', + 'none' => 'N/A', + ], + '#default_value' => ['sat'], + '#description' => 'Select Multiple', + ]; + + // Tel. + $form['phone'] = [ + '#type' => 'tel', + '#title' => $this->t('Phone'), + '#description' => $this->t('Tel, #type = tel'), + ]; + + // Details. + $form['details'] = [ + '#type' => 'details', + '#title' => $this->t('Details'), + '#description' => $this->t('Details, #type = details'), + ]; + + // TableSelect. + $options = [ + 1 => ['first_name' => 'Indy', 'last_name' => 'Jones'], + 2 => ['first_name' => 'Darth', 'last_name' => 'Vader'], + 3 => ['first_name' => 'Super', 'last_name' => 'Man'], + ]; + + $header = [ + 'first_name' => t('First Name'), + 'last_name' => t('Last Name'), + ]; + + $form['table'] = [ + '#type' => 'tableselect', + '#title' => $this->t('Users'), + '#header' => $header, + '#options' => $options, + '#empty' => t('No users found'), + ]; + + // Textarea. + $form['text'] = [ + '#type' => 'textarea', + '#title' => $this->t('Text'), + '#description' => $this->t('Textarea, #type = textarea'), + ]; + + // Text format. + $form['text_format'] = [ + '#type' => 'text_format', + '#title' => 'Text format', + '#format' => 'plain_text', + '#expected_value' => [ + 'value' => 'Text value', + 'format' => 'plain_text', + ], + '#textformat_value' => [ + 'value' => 'Testvalue', + 'format' => 'filtered_html', + ], + '#description' => $this->t('Text format, #type = text_format'), + ]; + + // Textfield. + $form['subject'] = [ + '#type' => 'textfield', + '#title' => t('Subject'), + '#size' => 60, + '#maxlength' => 128, + '#description' => $this->t('Textfield, #type = textfield'), + ]; + + // Weight. + $form['weight'] = [ + '#type' => 'weight', + '#title' => t('Weight'), + '#delta' => 10, + '#description' => $this->t('Weight, #type = weight'), + ]; + + // Group submit handlers in an actions element with a key of "actions" so + // that it gets styled correctly, and so that other modules may add actions + // to the form. + $form['actions'] = [ + '#type' => 'actions', + ]; + + // Extra actions for the display. + $form['actions']['extra_actions'] = [ + '#type' => 'dropbutton', + '#links' => [ + 'simple_form' => [ + 'title' => $this->t('Simple Form'), + 'url' => Url::fromRoute('form_api_example.simple_form'), + ], + 'demo' => [ + 'title' => $this->t('Build Demo'), + 'url' => Url::fromRoute('form_api_example.build_demo'), + ], + ], + ]; + + // File. + $form['file'] = [ + '#type' => 'file', + '#title' => 'File', + '#description' => $this->t('File, #type = file'), + ]; + + // Manage file. + $form['managed_file'] = [ + '#type' => 'managed_file', + '#title' => 'Managed file', + '#description' => $this->t('Manage file, #type = managed_file'), + ]; + + // Image Buttons. + $form['image_button'] = [ + '#type' => 'image_button', + '#value' => 'Image button', + '#src' => drupal_get_path('module', 'examples') . '/images/100x30.svg', + '#description' => $this->t('image file, #type = image_button'), + ]; + + // Button. + $form['button'] = [ + '#type' => 'button', + '#value' => 'Button', + '#description' => $this->t('Button, #type = button'), + ]; + + // Add a submit button that handles the submission of the form. + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + '#description' => $this->t('Submit, #type = submit'), + ]; + + // Add a reset button that handles the submission of the form. + $form['actions']['reset'] = [ + '#type' => 'button', + '#button_type' => 'reset', + '#value' => t('Reset'), + '#description' => $this->t('Submit, #type = button, #button_type = reset, #attributes = this.form.reset();return false'), + '#attributes' => [ + 'onclick' => 'this.form.reset(); return false;', + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'form_api_example_input_demo_form'; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Find out what was submitted. + $values = $form_state->getValues(); + foreach ($values as $key => $value) { + $label = isset($form[$key]['#title']) ? $form[$key]['#title'] : $key; + + // Many arrays return 0 for unselected values so lets filter that out. + if (is_array($value)) { + $value = array_filter($value); + } + + // Only display for controls that have titles and values. + if ($value && $label) { + $display_value = is_array($value) ? preg_replace('/[\n\r\s]+/', ' ', print_r($value, 1)) : $value; + $message = $this->t('Value for %title: %value', ['%title' => $label, '%value' => $display_value]); + drupal_set_message($message); + } + } + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/ModalForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/ModalForm.php new file mode 100644 index 000000000..b6ccaec7e --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/ModalForm.php @@ -0,0 +1,192 @@ +setRequestStack($container->get('request_stack')); + $form->setStringTranslation($container->get('string_translation')); + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'form_api_example_modal_form'; + } + + /** + * Helper method so we can have consistent dialog options. + * + * @return string[] + * An array of jQuery UI elements to pass on to our dialog form. + */ + protected static function getDataDialogOptions() { + return [ + 'width' => '50%', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $nojs = NULL) { + // Add the core AJAX library. + $form['#attached']['library'][] = 'core/drupal.ajax'; + + $form['description'] = [ + '#type' => 'item', + '#markup' => $this->t('This example demonstrates a form that can work as a normal multi-request form, or as a modal dialog using AJAX.'), + ]; + // Add a link to show this form in a modal dialog if we're not already in + // one. + if ($nojs == 'nojs') { + $form['use_ajax_container'] = [ + '#type' => 'details', + '#open' => TRUE, + ]; + $form['use_ajax_container']['description'] = [ + '#type' => 'item', + '#markup' => $this->t('In order to show a modal dialog by clicking on a link, that link has to have class use-ajax and data-dialog-type="modal". This link has those attributes.'), + ]; + $form['use_ajax_container']['use_ajax'] = [ + '#type' => 'link', + '#title' => $this->t('See this form as a modal.'), + '#url' => Url::fromRoute('form_api_example.modal_form', ['nojs' => 'ajax']), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode(static::getDataDialogOptions()), + // Add this id so that we can test this form. + 'id' => 'ajax-example-modal-link', + ], + ]; + } + + // This element is responsible for displaying form errors in the AJAX + // dialog. + if ($nojs == 'ajax') { + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -999, + ]; + } + + $form['title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Title'), + '#required' => TRUE, + ]; + + // Group submit handlers in an actions element with a key of "actions" so + // that it gets styled correctly, and so that other modules may add actions + // to the form. + $form['actions'] = [ + '#type' => 'actions', + ]; + + // Add a submit button that handles the submission of the form. + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + '#ajax' => [ + 'callback' => '::ajaxSubmitForm', + 'event' => 'click', + ], + ]; + + // Set the form to not use AJAX if we're on a nojs path. When this form is + // within the modal dialog, Drupal will make sure we're using an AJAX path + // instead of a nojs one. + if ($nojs == 'nojs') { + unset($form['actions']['submit']['#ajax']); + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $title = $form_state->getValue('title'); + $message = $this->t('Submit handler: You specified a title of @title.', ['@title' => $title]); + drupal_set_message($message); + } + + /** + * Implements the submit handler for the modal dialog AJAX call. + * + * @param array $form + * Render array representing from. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Current form state. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * Array of AJAX commands to execute on submit of the modal form. + */ + public function ajaxSubmitForm(array &$form, FormStateInterface $form_state) { + // We begin building a new ajax reponse. + $response = new AjaxResponse(); + + // If the user submitted the form and there are errors, show them the + // input dialog again with error messages. Since the title element is + // required, the empty string wont't validate and there will be an error. + if ($form_state->getErrors()) { + // If there are errors, we can show the form again with the errors in + // the status_messages section. + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -10, + ]; + $response->addCommand(new OpenModalDialogCommand($this->t('Errors'), $form, static::getDataDialogOptions())); + } + // If there are no errors, show the output dialog. + else { + // We don't want any messages that were added by submitForm(). + drupal_get_messages(); + // We use FormattableMarkup to handle sanitizing the input. + // @todo: There's probably a better way to do this. + $title = new FormattableMarkup(':title', [':title' => $form_state->getValue('title')]); + // This will be the contents for the modal dialog. + $content = [ + '#type' => 'item', + '#markup' => $this->t('Your specified title of \'%title\' appears in this modal dialog.', ['%title' => $title]), + ]; + // Add the OpenModalDialogCommand to the response. This will cause Drupal + // AJAX to show the modal dialog. The user can click the little X to close + // the dialog. + $response->addCommand(new OpenModalDialogCommand($title, $content, static::getDataDialogOptions())); + } + + // Finally return our response. + return $response; + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/MultistepForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/MultistepForm.php new file mode 100644 index 000000000..3bfacaf3e --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/MultistepForm.php @@ -0,0 +1,193 @@ +has('page_num') && $form_state->get('page_num') == 2) { + return self::fapiExamplePageTwo($form, $form_state); + } + + $form_state->set('page_num', 1); + + $form['description'] = [ + '#type' => 'item', + '#title' => $this->t('A basic multistep form (page 1)'), + ]; + + $form['first_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('First Name'), + '#description' => $this->t('Enter your first name.'), + '#default_value' => $form_state->getValue('first_name', ''), + '#required' => TRUE, + ]; + + $form['last_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Last Name'), + '#default_value' => $form_state->getValue('last_name', ''), + '#description' => $this->t('Enter your last name.'), + ]; + + $form['birth_year'] = [ + '#type' => 'number', + '#title' => $this->t('Birth Year'), + '#default_value' => $form_state->getValue('birth_year', ''), + '#description' => $this->t('Format is "YYYY" and value between 1900 and 2000'), + ]; + + // Group submit handlers in an actions element with a key of "actions" so + // that it gets styled correctly, and so that other modules may add actions + // to the form. This is not required, but is convention. + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['next'] = [ + '#type' => 'submit', + '#button_type' => 'primary', + '#value' => $this->t('Next'), + // Custom submission handler for page 1. + '#submit' => ['::fapiExampleMultistepFormNextSubmit'], + // Custom validation handler for page 1. + '#validate' => ['::fapiExampleMultistepFormNextValidate'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $page_values = $form_state->get('page_values'); + + drupal_set_message($this->t('The form has been submitted. name="@first @last", year of birth=@year_of_birth', [ + '@first' => $page_values['first_name'], + '@last' => $page_values['last_name'], + '@year_of_birth' => $page_values['birth_year'], + ])); + + drupal_set_message($this->t('And the favorite color is @color', ['@color' => $form_state->getValue('color')])); + } + + /** + * Provides custom validation handler for page 1. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function fapiExampleMultistepFormNextValidate(array &$form, FormStateInterface $form_state) { + $birth_year = $form_state->getValue('birth_year'); + + if ($birth_year != '' && ($birth_year < 1900 || $birth_year > 2000)) { + // Set an error for the form element with a key of "birth_year". + $form_state->setErrorByName('birth_year', $this->t('Enter a year between 1900 and 2000.')); + } + } + + /** + * Provides custom submission handler for page 1. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function fapiExampleMultistepFormNextSubmit(array &$form, FormStateInterface $form_state) { + $form_state + ->set('page_values', [ + // Keep only first step values to minimize stored data. + 'first_name' => $form_state->getValue('first_name'), + 'last_name' => $form_state->getValue('last_name'), + 'birth_year' => $form_state->getValue('birth_year'), + ]) + ->set('page_num', 2) + ->setRebuild(TRUE); + } + + /** + * Builds the second step form (page 2). + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The render array defining the elements of the form. + */ + public function fapiExamplePageTwo(array &$form, FormStateInterface $form_state) { + + $form['description'] = [ + '#type' => 'item', + '#title' => $this->t('A basic multistep form (page 2)'), + ]; + + $form['color'] = [ + '#type' => 'textfield', + '#title' => $this->t('Favorite color'), + '#required' => TRUE, + '#default_value' => $form_state->getValue('color', ''), + ]; + $form['back'] = [ + '#type' => 'submit', + '#value' => $this->t('Back'), + // Custom submission handler for 'Back' button. + '#submit' => ['::fapiExamplePageTwoBack'], + // We won't bother validating the required 'color' field, since they + // have to come back to this page to submit anyway. + '#limit_validation_errors' => [], + ]; + $form['submit'] = [ + '#type' => 'submit', + '#button_type' => 'primary', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * Provides custom submission handler for 'Back' button (page 2). + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function fapiExamplePageTwoBack(array &$form, FormStateInterface $form_state) { + $form_state + // Restore values for the first step. + ->setValues($form_state->get('page_values')) + ->set('page_num', 1) + ->setRebuild(TRUE); + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/SimpleForm.php b/sites/all/modules/examples/examples/form_api_example/src/Form/SimpleForm.php new file mode 100644 index 000000000..5b105362f --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/SimpleForm.php @@ -0,0 +1,114 @@ + 'item', + '#markup' => $this->t('This basic example shows a single text input element and a submit button'), + ]; + + $form['title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Title'), + '#description' => $this->t('Title must be at least 5 characters in length.'), + '#required' => TRUE, + ]; + + // Group submit handlers in an actions element with a key of "actions" so + // that it gets styled correctly, and so that other modules may add actions + // to the form. This is not required, but is convention. + $form['actions'] = [ + '#type' => 'actions', + ]; + + // Add a submit button that handles the submission of the form. + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * Getter method for Form ID. + * + * The form ID is used in implementations of hook_form_alter() to allow other + * modules to alter the render array built by this form controller. it must + * be unique site wide. It normally starts with the providing module's name. + * + * @return string + * The unique ID of the form defined by this class. + */ + public function getFormId() { + return 'form_api_example_simple_form'; + } + + /** + * Implements form validation. + * + * The validateForm method is the default method called to validate input on + * a form. + * + * @param array $form + * The render array of the currently built form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Object describing the current state of the form. + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $title = $form_state->getValue('title'); + if (strlen($title) < 5) { + // Set an error for the form element with a key of "title". + $form_state->setErrorByName('title', $this->t('The title must be at least 5 characters long.')); + } + } + + /** + * Implements a form submit handler. + * + * The submitForm method is the default method called for any submit elements. + * + * @param array $form + * The render array of the currently built form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Object describing the current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + /* + * This would normally be replaced by code that actually does something + * with the title. + */ + $title = $form_state->getValue('title'); + drupal_set_message(t('You specified a title of %title.', ['%title' => $title])); + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/StateDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/StateDemo.php new file mode 100644 index 000000000..e047f2c85 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/StateDemo.php @@ -0,0 +1,89 @@ + 'item', + '#markup' => $this->t('This example demonstrates the #states property. #states makes an element visibility dependent on another.'), + ]; + + $form['needs_accommodation'] = [ + '#type' => 'checkbox', + '#title' => 'Need Special Accommodations?', + ]; + + // The #states property used here binds the visibility of the of the + // container element to the value of the needs_accommodation checkbox above. + $form['accommodation'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => 'accommodation', + ], + '#states' => [ + 'invisible' => [ + 'input[name="needs_accommodation"]' => ['checked' => FALSE], + ], + ], + ]; + + $form['accommodation']['diet'] = [ + '#type' => 'textfield', + '#title' => t('Dietary Restrictions'), + ]; + + // Add a submit button that handles the submission of the form. + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * Getter method for Form ID. + * + * @inheritdoc + */ + public function getFormId() { + return 'form_api_example_state_demo'; + } + + /** + * Implements submitForm callback. + * + * @inheritdoc + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Find out what was submitted. + $values = $form_state->getValues(); + if ($values['needs_accommodation']) { + drupal_set_message($this->t('Dietary Restriction Requested: %diet', ['%diet' => $values['diet']])); + } + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/src/Form/VerticalTabsDemo.php b/sites/all/modules/examples/examples/form_api_example/src/Form/VerticalTabsDemo.php new file mode 100644 index 000000000..4a6d35dd3 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/src/Form/VerticalTabsDemo.php @@ -0,0 +1,76 @@ + 'item', + '#markup' => $this->t('This example demonstrates the use of vertical tabs to group elements.'), + ]; + + $form['information'] = [ + '#type' => 'vertical_tabs', + '#default_tab' => 'edit-publication', + ]; + + $form['author'] = [ + '#type' => 'details', + '#title' => 'Author', + '#group' => 'information', + ]; + + $form['author']['name'] = [ + '#type' => 'textfield', + '#title' => t('Name'), + ]; + + $form['publication'] = [ + '#type' => 'details', + '#title' => t('Publication'), + '#group' => 'information', + ]; + + $form['publication']['publisher'] = [ + '#type' => 'textfield', + '#title' => t('Publisher'), + ]; + + $form['actions'] = ['#type' => 'actions']; + // Add a submit button that handles the submission of the form. + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * Getter method for Form ID. + * + * @inheritdoc + */ + public function getFormId() { + return 'form_api_example_vertical_tabs_demo'; + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/templates/description.html.twig b/sites/all/modules/examples/examples/form_api_example/templates/description.html.twig new file mode 100644 index 000000000..6740778c5 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/templates/description.html.twig @@ -0,0 +1,31 @@ +{# + +Description text for the Fapi Example. + +#} + +{{ attach_library('core/drupal.dialog.ajax') }} +{% set simple_form = path('form_api_example.simple_form') %} +{% set multistep_form = path('form_api_example.multistep_form') %} +{% set input_demo = path('form_api_example.input_demo') %} +{% set build_demo = path('form_api_example.build_demo') %} +{% set container_demo = path('form_api_example.container_demo') %} +{% set state_demo = path('form_api_example.state_demo') %} +{% set vertical_tabs_demo = path('form_api_example.vertical_tabs_demo') %} +{% set ajax_demo = path('form_api_example.ajax_color_demo') %} +{% set ajax_addmore = path('form_api_example.ajax_addmore') %} +{% set modal_form = path('form_api_example.modal_form') %} +{% trans %} + +

Form examples to demonstrate common UI solutions using the Drupal Form API

+

Simple form

+

Multistep form

+

Common input elements

+

Build form demo

+

Container elements

+

Form state binding

+

Vertical tab elements

+

Ajax form

+

Add-more button

+

Modal form

+{% endtrans %} diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/FapiExampleTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/FapiExampleTest.php new file mode 100644 index 000000000..a30f6a7a6 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/FapiExampleTest.php @@ -0,0 +1,325 @@ +doTestRoutes(); + + $this->doTestAjaxAddMore(); + $this->doTestAjaxColorForm(); + $this->doTestBuildDemo(); + $this->doTestContainerDemoForm(); + $this->doTestInputDemoForm(); + $this->doTestModalForm(); + $this->doTestSimpleFormExample(); + $this->doTestStateDemoForm(); + $this->doTestVerticalTabsDemoForm(); + } + + /** + * Tests links. + */ + public function doTestRoutes() { + $assertion = $this->assertSession(); + + // Routes with menu links, and their form buttons. + $routes = [ + 'form_api_example.description' => [], + 'form_api_example.simple_form' => ['Submit'], + 'form_api_example.input_demo' => ['Submit'], + 'form_api_example.state_demo' => ['Submit'], + 'form_api_example.container_demo' => ['Submit'], + 'form_api_example.vertical_tabs_demo' => ['Submit'], + // Modal form has a submit button, but requires input. + 'form_api_example.modal_form' => [], + 'form_api_example.ajax_color_demo' => ['Submit'], + 'form_api_example.build_demo' => ['Submit'], + 'form_api_example.ajax_addmore' => ['Submit'], + // Multistep form has submit buttons, but requires input. + 'form_api_example.multistep_form' => [], + ]; + + // Ensure the links appear in the tools menu sidebar. + $this->drupalGet(''); + foreach (array_keys($routes) as $route) { + $assertion->linkByHrefExists(Url::fromRoute($route)->getInternalPath()); + } + + // Go to all the routes and click all the buttons. + foreach ($routes as $route => $buttons) { + $path = Url::fromRoute($route); + error_log($route); + $this->drupalGet($path); + $assertion->statusCodeEquals(200); + foreach ($buttons as $button) { + $this->drupalPostForm($path, [], $button); + $assertion->statusCodeEquals(200); + } + } + } + + /** + * Test the ajax demo form. + */ + public function doTestAjaxColorForm() { + $assert = $this->assertSession(); + + // Post the form. + $edit = [ + 'temperature' => 'warm', + ]; + $this->drupalPostForm(Url::fromRoute('form_api_example.ajax_color_demo'), $edit, 'Submit'); + $assert->statusCodeEquals(200); + $assert->pageTextContains('Value for Temperature: warm'); + } + + /** + * Test the build demo form. + */ + public function doTestBuildDemo() { + $assert = $this->assertSession(); + $build_demo_url = Url::fromRoute('form_api_example.build_demo'); + + $edit = [ + 'change' => '1', + ]; + $this->drupalPostForm($build_demo_url, $edit, 'Submit'); + + $assert->pageTextContains('1. __construct'); + $assert->pageTextContains('2. getFormId'); + $assert->pageTextContains('3. validateForm'); + $assert->pageTextContains('4. submitForm'); + + // Ensure the 'submit rebuild' action performs the rebuild. + $this->drupalPostForm($build_demo_url, $edit, 'Submit Rebuild'); + $assert->pageTextContains('4. rebuildFormSubmit'); + } + + /** + * Test the container demo form. + */ + public function doTestContainerDemoForm() { + $assert = $this->assertSession(); + + // Post the form. + $edit = [ + 'name' => 'Dave', + 'pen_name' => 'DMan', + 'title' => 'My Book', + 'publisher' => 'me', + 'diet' => 'vegan', + ]; + $this->drupalPostForm(Url::fromRoute('form_api_example.container_demo'), $edit, 'Submit'); + $assert->pageTextContains('Value for name: Dave'); + $assert->pageTextContains('Value for pen_name: DMan'); + $assert->pageTextContains('Value for title: My Book'); + $assert->pageTextContains('Value for publisher: me'); + $assert->pageTextContains('Value for diet: vegan'); + } + + /** + * Test the input demo form. + */ + public function doTestInputDemoForm() { + $assert = $this->assertSession(); + + // Post the form. + $edit = [ + 'tests_taken[SAT]' => TRUE, + 'color' => '#2b49ff', + 'expiration' => '2015-10-21', + 'datetime[date]' => '2017-12-07 15:32:10', + 'url' => 'https://www.drupal.org', + 'email' => 'somebody@example.org', + 'quantity' => '4', + 'password' => 'letmein', + 'password_confirm[pass1]' => 'letmein', + 'password_confirm[pass2]' => 'letmein', + 'size' => '76', + 'active' => '1', + 'search' => 'my search string', + 'favorite' => 'blue', + 'select_multiple[]' => ['act'], + 'phone' => '555-555-5555', + 'table[1]' => TRUE, + 'table[3]' => TRUE, + 'text' => 'This is a test of my form.', + 'text_format[value]' => 'Examples for Developers', + 'subject' => 'Form test', + 'weight' => '3', + ]; + $this->drupalPostForm(Url::fromRoute('form_api_example.input_demo'), $edit, 'Submit'); + $assert->statusCodeEquals(200); + + $assert->pageTextContains('Value for What standardized tests did you take?'); + $assert->pageTextContains('Value for Color: #2b49ff'); + $assert->pageTextContains('Value for Content expiration: 2015-10-21'); + $assert->pageTextContains('Value for Date Time: 2017-12-07 15:32:10'); + $assert->pageTextContains('Value for URL: https://www.drupal.org'); + $assert->pageTextContains('Value for Email: somebody@example.org'); + $assert->pageTextContains('Value for Quantity: 4'); + $assert->pageTextContains('Value for Password: letmein'); + $assert->pageTextContains('Value for New Password: letmein'); + $assert->pageTextContains('Value for Size: 76'); + $assert->pageTextContains('Value for active: 1'); + $assert->pageTextContains('Value for Search: my search string'); + $assert->pageTextContains('Value for Favorite color: blue'); + $assert->pageTextContains('Value for Select (multiple): Array ( [act] => act )'); + $assert->pageTextContains('Value for Phone: 555-555-5555'); + $assert->pageTextContains('Value for Users: Array ( [1] => 1 [3] => 3 )'); + $assert->pageTextContains('Value for Text: This is a test of my form.'); + $assert->pageTextContains('Value for Text format: Array ( [value] => Examples for Developers [format] => plain_text )'); + $assert->pageTextContains('Value for Subject: Form test'); + $assert->pageTextContains('Value for Weight: 3'); + } + + /** + * Test the modal form. + */ + public function doTestModalForm() { + $assert = $this->assertSession(); + + // Post the form. + $edit = [ + 'title' => 'My Book', + ]; + $this->drupalPostForm(Url::fromRoute('form_api_example.modal_form'), $edit, 'Submit'); + $assert->pageTextContains('Submit handler: You specified a title of My Book.'); + } + + /** + * Check routes defined by form_api_example. + */ + public function doTestSimpleFormExample() { + $assert = $this->assertSession(); + + // Post a title. + $edit = ['title' => 'My Custom Title']; + $this->drupalPostForm(Url::fromRoute('form_api_example.simple_form'), $edit, 'Submit'); + $assert->pageTextContains('You specified a title of My Custom Title.'); + } + + /** + * Test the state demo form. + */ + public function doTestStateDemoForm() { + $assert = $this->assertSession(); + + // Post the form. + $edit = [ + 'needs_accommodation' => TRUE, + 'diet' => 'vegan', + ]; + $this->drupalPostForm(Url::fromRoute('form_api_example.state_demo'), $edit, 'Submit'); + $assert->pageTextContains('Dietary Restriction Requested: vegan'); + } + + /** + * Test the vertical tabs demo form. + */ + public function doTestVerticalTabsDemoForm() { + $assert = $this->assertSession(); + + // Post the form. + $edit = [ + 'name' => 'Dave', + 'publisher' => 'me', + ]; + $this->drupalPostForm(Url::fromRoute('form_api_example.container_demo'), $edit, 'Submit'); + $assert->pageTextContains('Value for name: Dave'); + $assert->pageTextContains('Value for publisher: me'); + } + + /** + * Test the Ajax Add More demo form. + */ + public function doTestAjaxAddMore() { + // XPath for the remove button. We have to use contains() here because the + // ID will have a hash value at the end. + $button_xpath = '//input[contains(@id,"edit-names-fieldset-actions-remove-name")]'; + + $ajax_addmore_url = Url::fromRoute('form_api_example.ajax_addmore'); + + // Verify that anonymous can access the ajax_add_more page. + $this->drupalGet($ajax_addmore_url); + $this->assertResponse(200, 'The Demo of Container page is available.'); + // Verify that there is no remove button. + $this->assertFalse($this->xpath($button_xpath)); + + $name_one = 'John'; + $name_two = 'Smith'; + + // Enter the value in field-1. + // and click on 'Add one more' button. + $edit = []; + $edit['names_fieldset[name][0]'] = $name_one; + $this->drupalPostForm($ajax_addmore_url, $edit, 'Add one more'); + + // Verify field-2 gets added. + // and value of field-1 should retained. + $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-0"]'), $name_one); + $this->assertTrue($this->xpath('//input[@id = "edit-names-fieldset-name-1"]')); + // Verify that the remove button was added. + $this->assertTrue($this->xpath($button_xpath)); + + // Enter the value in field-2 + // and click on 'Add one more' button. + $edit['names_fieldset[name][1]'] = $name_two; + $this->drupalPostForm(NULL, $edit, 'Add one more'); + + // Verify field-3 gets added. + // and value of field-1 and field-2 are retained. + $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-0"]'), $name_one); + $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-1"]'), $name_two); + $this->assertTrue($this->xpath('//input[@id = "edit-names-fieldset-name-2"]')); + + // Click on "Remove one" button to test remove button works. + // and value of field-1 and field-2 are retained. + $this->drupalPostForm(NULL, NULL, 'Remove one'); + $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-0"]'), $name_one); + $this->assertFieldsByValue($this->xpath('//input[@id = "edit-names-fieldset-name-1"]'), $name_two); + $this->assertFalse($this->xpath('//input[@id = "edit-names-fieldset-name-2"]')); + + // Submit the form and verify the results. + $this->drupalPostForm(NULL, NULL, 'Submit'); + $this->assertText('These people are coming to the picnic: ' . $name_one . ', ' . $name_two); + + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/MultistepFormTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/MultistepFormTest.php new file mode 100644 index 000000000..95c1226e7 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/tests/src/Functional/MultistepFormTest.php @@ -0,0 +1,68 @@ +drupalGet(Url::fromRoute('form_api_example.multistep_form')); + $page = $this->getSession()->getPage(); + $h1 = $page->find('css', 'h1'); + $this->assertContains('Multistep form', $h1->getText()); + $desc = $page->find('css', '#edit-description label'); + $this->assertContains('page 1', $desc->getText()); + $this->submitForm([ + 'first_name' => 'Bozo', + 'last_name' => 'Di Clown', + 'birth_year' => 1980, + ], + 'Next'); + + // Really new page? + $page2 = $this->getSession()->getPage(); + $desc = $page2->find('css', '#edit-description label'); + $this->assertContains('page 2', $desc->getText()); + + // Try the back button. + $this->submitForm([], 'Back'); + $page1 = $this->getSession()->getPage(); + $desc = $page1->find('css', '#edit-description label'); + $this->assertContains('page 1', $desc->getText()); + // Is the form still filled out? + $first_name = $page1->findField('first_name')->getValue(); + $this->assertEquals('Bozo', $first_name); + $second_name = $page1->findField('last_name')->getValue(); + $this->assertEquals('Di Clown', $second_name); + $birth_year = $page1->findField('birth_year')->getValue(); + $this->assertEquals('1980', $birth_year); + + // Back to the second page. + $this->click('#edit-next'); + $page2 = $this->getSession()->getPage(); + $desc = $page2->find('css', '#edit-description label'); + $this->assertContains('page 2', $desc->getText()); + $this->submitForm(['color' => 'neon green'], 'Submit'); + + // This should take us back to the first page with a status message. + $messages = $this->getSession()->getPage()->find('css', 'ul.messages__list'); + $message_text = $messages->getHtml(); + $this->assertContains('Bozo Di Clown', $message_text); + $this->assertContains('1980', $message_text); + $this->assertContains('neon green', $message_text); + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/AjaxColorFormTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/AjaxColorFormTest.php new file mode 100644 index 000000000..afe157369 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/AjaxColorFormTest.php @@ -0,0 +1,83 @@ +drupalGet(Url::fromRoute('form_api_example.ajax_color_demo')); + + // Get Mink stuff. + $assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Before the color temperature dropdown is selected, we should not have a + // color dropdown. + $this->assertEmpty($page->find('css', 'select[name="color"]')); + + $color_matrix = [ + 'warm' => ['red', 'orange', 'yellow'], + 'cool' => ['blue', 'purple', 'green'], + ]; + + foreach ($color_matrix as $temperature => $colors) { + // Enter a color temperature. + $this->assertNotEmpty( + $color_temperature = $page->find('css', 'select[name="temperature"]') + ); + $color_temperature->setValue($temperature); + $assert->assertWaitOnAjaxRequest(); + + // Find the color dropdown. + $this->assertNotEmpty( + $color_select = $page->find('css', 'select[name="color"]') + ); + + // Make sure all the correct color options are present. + $this->assertNotEmpty( + $color_options = $color_select->findAll('css', 'option') + ); + $this->assertCount(count($colors), $color_options); + foreach ($color_options as $color_element) { + $this->assertContains($color_element->getValue(), $colors); + } + + // Submit all the colors. + foreach ($colors as $color) { + $page->find('css', 'select[name="temperature"]')->setValue($temperature); + $assert->assertWaitOnAjaxRequest(); + $page->find('css', 'select[name="color"]')->setValue($color); + $page->findButton('Submit')->click(); + $assert->pageTextContains("Value for Temperature: $temperature"); + $assert->pageTextContains("Value for color: $color"); + } + } + + // Finally, we can make sure that when we 'unset' the temperature dropdown, + // the color dropdown goes away. + $this->getSession()->getPage()->find('css', 'select[name="temperature"]') + ->setValue(''); + $assert->assertWaitOnAjaxRequest(); + $this->assertEmpty($page->find('css', 'select[name="color"]')); + } + +} diff --git a/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/ModalFormTest.php b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/ModalFormTest.php new file mode 100644 index 000000000..3322bcf14 --- /dev/null +++ b/sites/all/modules/examples/examples/form_api_example/tests/src/FunctionalJavascript/ModalFormTest.php @@ -0,0 +1,75 @@ + 'nojs']); + $this->drupalGet($modal_route_nojs); + + // Get Mink stuff. + $assert = $this->assertSession(); + $session = $this->getSession(); + $page = $this->getSession()->getPage(); + + // Click on 'see this form as a modal'. + $this->clickLink('ajax-example-modal-link'); + + $this->assertNotEmpty($assert->waitForElementVisible('css', '.ui-dialog')); + + // Enter a value. + $this->assertNotEmpty($input = $page->find('css', 'div.ui-dialog input[name="title"]')); + $input->setValue('test_title'); + + // Click 'submit'. + // @todo: Switch to using NodeElement::click() on the button or + // NodeElement::submit() on the form when #2831506 is fixed. + // @see https://www.drupal.org/node/2831506 + $session->executeScript("jQuery('button.ui-button.form-submit').click()"); + $assert->assertWaitOnAjaxRequest(); + + // Check that we have a new modal. + $assert->elementContains('css', 'span.ui-dialog-title', 'test_title'); + + // Click the close X. + // @todo: Switch to using NodeElement::click() on the button or + // NodeElement::submit() on the form when #2831506 is fixed. + // @see https://www.drupal.org/node/2831506 + $session->executeScript("jQuery('button.ui-dialog-titlebar-close').click()"); + $assert->assertWaitOnAjaxRequest(); + + // Verify that the modal went away. + $assert->pageTextNotContains('appears in this modal dialog.'); + } + +} diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.api.php b/sites/all/modules/examples/examples/hooks_example/hooks_example.api.php new file mode 100644 index 000000000..c3a1a2d6b --- /dev/null +++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.api.php @@ -0,0 +1,53 @@ + $node->label()])); + } +} + +/** + * @} End of "addtogroup hooks_example". + */ diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.info.yml b/sites/all/modules/examples/examples/hooks_example/hooks_example.info.yml new file mode 100644 index 000000000..b27b410ea --- /dev/null +++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.info.yml @@ -0,0 +1,14 @@ +name: Hooks Example +type: module +description: Provides examples of implementing, defining, and invoking hooks. +package: Example modules +# core: 8.x +dependencies: + - drupal:help + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.module b/sites/all/modules/examples/examples/hooks_example/hooks_example.module new file mode 100644 index 000000000..8a5f2f13a --- /dev/null +++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.module @@ -0,0 +1,226 @@ +' . t('This text is provided by the function hooks_example_help(), which is an implementation of the hook hook_help(). To learn more about how this works checkout the code in hooks_example.module.') . '

'; + } +} + +/** + * Implements hook_ENTITY_TYPE_view(). + * + * Some hook names include additional tokens that need to be replaced when + * implementing the hook. These hooks are dynamic in that when they are being + * invoked a portion of their name is replaced with a dynamic value. This is + * indicated by placing the token words in all caps. This pattern is often used + * in situations where you want to allow modules to generically act on all + * instances of a thing, or to act on only a specific subset. + * + * There are lots of different entity types in Drupal. Node, user, file, etc. + * Using hook_entity_view() a module can act on a any entity that is being + * viewed, regardless of type. If we wanted to count views of all entities, + * regardless of type this would be a good choice. This variant is also useful + * if you want to provide administrators with a form where they can choose from + * a list of entity types which ones they want to count views for. The logic in + * the generic hook implementation could then take that into account and act on + * only a select set of entity types. + * + * If however, you know you only ever want to act on viewing of a node entity + * you can instead implement hook_ENTITY_TYPE_view(). Where ENTITY_TYPE is a + * token that can be replaced with any valid entity type name. + * + * @see hook_entity_view() + * @see hook_ENTITY_TYPE_view() + */ +function hooks_example_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { + // This example hook implementation keeps track of the number of times a user + // has viewed a specific node during their current session. Then displays that + // information for them when they view a node. + // + // In addition, a hook is invoked that allows other modules to react when the + // page view count is updated. + // + // Retrieve the active session from the current request object. + $session = \Drupal::request()->getSession(); + $current_counts = $session->get('hooks_example.view_counts', []); + if (!isset($current_counts[$entity->id()])) { + // If this is the first time they've viewed the page we need to start the + // counter. + $current_counts[$entity->id()] = 1; + } + else { + // If they have already viewed this page just increment the existing + // counter. + $current_counts[$entity->id()]++; + } + + // Save the updated values. + $session->set('hooks_example.view_counts', $current_counts); + + // Invoke a hook to alert other modules that the count was updated. + // + // Hooks are invoked via the `module_handler` service. Which is an instance of + // \Drupal\Core\Extension\ModuleHandlerInterface. + // + // Hooks can be invoked in a few different ways: + // - All at once using ModuleHandlerInterface::invokeAll() to call all + // implementations of the specified hook provided by any enabled module. + // - One at a time using ModuleHandlerInterface::invoke() to call only the + // the specified module's implementation of a hook. + // - Using ModuleHandlerInterface::alter() to pass alterable variables to + // hook_TYPE_alter() implementations for all enabled modules. This method + // should be used for instances where the calling module has assembled data + // and would like to give other modules an opportunity to alter that data + // before it's used. A common pattern is to use invokeAll() to first gather + // input from other modules, the immediately afterwards call alter() to give + // modules the opportunity to alter the aggregate data. + $module_handler = \Drupal::moduleHandler(); + + // Calling \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll() will + // call implementations of the hook in question for all enabled modules. The + // method takes two arguments. The name of the hook to invoke, and an optional + // array of arguments to pass to any functions implementing the hook. + // + // Hook names need to be unique. So when defining a new hook in your module it + // is customary to prefix the hook name with the short name of your module + // followed by the descriptive name of the hook itself. Because hooks names + // are also PHP function names they should contain only lowercase alphanumeric + // characters and underscores. + // + // The hook name parameter should have the "hook_" prefix removed. So if you + // want to invoke hook_mymodule_do_something() the value used here would be + // 'mymodule_do_something'. + // + // Hook implementations can optionally return a value, depending on the hook + // definition. If they do, the invokeAll() method aggregates the responses + // from all hooks in an array and returns the array. + // + // In this example we're invoking hook_hooks_example_count_incremented() and + // passing all implementations the current view count for the node, and the + // node object itself. + $module_handler->invokeAll('hooks_example_count_incremented', [$current_counts[$entity->id()], $entity]); + + // Display the current number of pages the user has viewed along with the + // node's content. + $build['view_count'] = [ + '#markup' => '

' . t('You have viewed this node @total times this session.', ['@total' => $current_counts[$entity->id()]]) . '

', + // In order for this example to work we disable caching for the content of + // this node completely. This ensures that our hook is called every time the + // node is viewed instead of using a cached version of the page for + // subsequent requests. + '#cache' => [ + 'max-age' => 0, + ], + ]; +} + +/** + * Implements hook_form_alter(). + */ +function hooks_example_form_alter(&$form, FormStateInterface $form_state, $form_id) { + // This is an example of what is known as an alter hook. The $form paremater + // in this case represents an already complete Form API array and our hook + // implementation is being given the opportunity to make changes to the + // existing data structure before it's used. Inovking and alter hooks is a + // common pattern anytime lists or complex data structures are assembled. + // hook_form_alter(), which allows you to manipulate any form, is one of the + // most commonly implemented hooks. + // + // @see hook_form_alter() + // @see hook_form_FORM_ID_alter() + // + // If this is the user login form, change the description text of the username + // field. + if ($form_id === 'user_login_form') { + $form['name']['#description'] = t('This text has been altered by hooks_example_form_alter().'); + } +} + +/** + * Implements hook_hooks_example_count_incremented(). + * + * Hooks can be implemented by both the module that invokes them like we are + * doing here, as well as by any other enabled module. + */ +function hooks_example_hooks_example_count_incremented($current_count, NodeInterface $node) { + if ($current_count === 1) { + drupal_set_message(t('This is the first time you have viewed the node %title.', ['%title' => $node->label()])); + } +} + +/** + * @} End of "defgroup hooks_example". + */ diff --git a/sites/all/modules/examples/examples/hooks_example/hooks_example.routing.yml b/sites/all/modules/examples/examples/hooks_example/hooks_example.routing.yml new file mode 100644 index 000000000..76bd59cb5 --- /dev/null +++ b/sites/all/modules/examples/examples/hooks_example/hooks_example.routing.yml @@ -0,0 +1,8 @@ +# hooks_example only has one route. It is to a page explaining the module. +hooks_example.description: + path: 'examples/hooks-example' + defaults: + _controller: '\Drupal\hooks_example\Controller\HooksExampleController::description' + _title: 'Hooks Example' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/hooks_example/src/Controller/HooksExampleController.php b/sites/all/modules/examples/examples/hooks_example/src/Controller/HooksExampleController.php new file mode 100644 index 000000000..548422a25 --- /dev/null +++ b/sites/all/modules/examples/examples/hooks_example/src/Controller/HooksExampleController.php @@ -0,0 +1,23 @@ +Implementing, defining, and invoking hooks + +

The code in this module implements a couple of new features which you can see +in action.

+ +

Implements hook_help(): In hooks_example.module you'll find +an implementation of the hook hook_help() which is used to add contents to this +module's help overview. Visible at admin/help/hooks_example.

+ +

View counts: hooks_example_node_view() is an implementation +of the hook hook_ENTITY_TYPE_view() that adds a basic page view counter. You can +see this in action by navigating to any node on the site and looking for the the +text telling you how many times you've viewed that page.

+ +

Implements hook_form_alter(): In hookse_example.module +you'll find an implementation of hook_form_alter() which demonstrates the use of +one of the most commonly used alter hooks. You can view the altered form at +user/login.

+ +

To learn more about how to implement an existing hook, or how to define and +invoke a new hook start by reading the @docblock comments in +hooks_example.module

+{% endtrans %} diff --git a/sites/all/modules/examples/examples/hooks_example/tests/src/Functional/HooksExampleTest.php b/sites/all/modules/examples/examples/hooks_example/tests/src/Functional/HooksExampleTest.php new file mode 100644 index 000000000..7158c7093 --- /dev/null +++ b/sites/all/modules/examples/examples/hooks_example/tests/src/Functional/HooksExampleTest.php @@ -0,0 +1,84 @@ +createContentType(['type' => 'page']); + + $account = $this->drupalCreateUser(['access administration pages']); + $this->drupalLogin($account); + } + + /** + * Test the output of the example page. + */ + public function testHooksExample() { + // Test the description page at examples/hook-example returns a 200. + $this->drupalGet('examples/hooks-example'); + $this->assertSession()->statusCodeEquals(200); + + // Test that our implementation of hook_help() works. + $this->drupalGet('admin/help/hooks_example'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains('This text is provided by the function hooks_example_help()'); + + // Test that our implementation of hook_node_view() works. + // Create a new node. + $settings = [ + 'type' => 'page', + 'title' => 'Hooks Example Testing Node', + 'status' => 1, + ]; + $node = $this->drupalCreateNode($settings); + + $this->drupalGet($node->toUrl()); + + // Test that the output added to the page by hooks_example_node_view() is + // present. Which also tests that our page view counting was initialized. + $this->assertSession()->pageTextContains('You have viewed this node 1 times this session.'); + // Tests that the message set by + // hooks_example_hooks_example_count_incremented() is displayed on the page. + // Which also has the effect of testing to see wehther or not our custom + // hook is being invoked. + $this->assertSession()->pageTextContains('This is the first time you have viewed the node ' . $node->label() . '.'); + + // Navigate to a new page, and then back and verify the counter was updated. + $this->drupalGet(''); + $this->drupalGet($node->toUrl()); + $this->assertSession()->pageTextContains('You have viewed this node 2 times this session.'); + $this->assertSession()->pageTextNotContains('This is the first time you have viewed the node ' . $node->label() . '.'); + + // Test our implementation of hook_form_alter(). + $this->drupalLogout(); + $this->drupalGet('user/login'); + $this->assertSession()->pageTextContains('This text has been altered by hooks_example_form_alter().'); + } + +} diff --git a/sites/all/modules/examples/examples/images/100x30.svg b/sites/all/modules/examples/examples/images/100x30.svg new file mode 100644 index 000000000..3c1b33e17 --- /dev/null +++ b/sites/all/modules/examples/examples/images/100x30.svg @@ -0,0 +1,13 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + diff --git a/sites/all/modules/examples/examples/images/examples.svg b/sites/all/modules/examples/examples/images/examples.svg new file mode 100644 index 000000000..49e778305 --- /dev/null +++ b/sites/all/modules/examples/examples/images/examples.svg @@ -0,0 +1,56 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/sites/all/modules/examples/examples/js_example/css/jsweights.css b/sites/all/modules/examples/examples/js_example/css/jsweights.css new file mode 100755 index 000000000..b132f400c --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/css/jsweights.css @@ -0,0 +1,4 @@ +.js-weights div { + font-size: 20px; + font-weight: bold; +} diff --git a/sites/all/modules/examples/examples/js_example/js/black.js b/sites/all/modules/examples/examples/js_example/js/black.js new file mode 100755 index 000000000..038ba8bb0 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js/black.js @@ -0,0 +1,20 @@ +/** + * @file + * Contains the definition of the behaviour jsTestBlackWeight. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the JS test behavior to to weight div. + */ + Drupal.behaviors.jsTestBlackWeight = { + attach: function (context, settings) { + var weight = drupalSettings.js_example.js_weights.black; + var newDiv = $('
').css('color', 'black').html('I have a weight of ' + weight); + $('#js-weights').append(newDiv); + } + }; +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/examples/examples/js_example/js/blue.js b/sites/all/modules/examples/examples/js_example/js/blue.js new file mode 100755 index 000000000..88a2497ac --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js/blue.js @@ -0,0 +1,20 @@ +/** + * @file + * Contains the definition of the behaviour jsTestBlueWeight. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the JS test behavior to weight div. + */ + Drupal.behaviors.jsTestBlueWeight = { + attach: function (context, settings) { + var weight = drupalSettings.js_example.js_weights.blue; + var newDiv = $('
').css('color', 'blue').html('I have a weight of ' + weight); + $('#js-weights').append(newDiv); + } + }; +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/examples/examples/js_example/js/brown.js b/sites/all/modules/examples/examples/js_example/js/brown.js new file mode 100755 index 000000000..9c25b3a7f --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js/brown.js @@ -0,0 +1,20 @@ +/** + * @file + * Contains the definition of the behaviour jsTestBrownWeight. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the JS test behavior to weight div. + */ + Drupal.behaviors.jsTestBrownWeight = { + attach: function (context, settings) { + var weight = drupalSettings.js_example.js_weights.brown; + var newDiv = $('
').css('color', 'brown').html('I have a weight of ' + weight); + $('#js-weights').append(newDiv); + } + }; +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/examples/examples/js_example/js/green.js b/sites/all/modules/examples/examples/js_example/js/green.js new file mode 100755 index 000000000..2c88df4a9 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js/green.js @@ -0,0 +1,20 @@ +/** + * @file + * Contains the definition of the behaviour jsTestGreenWeight. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the JS test behavior to weight div. + */ + Drupal.behaviors.jsTestGreenWeight = { + attach: function (context, settings) { + var weight = drupalSettings.js_example.js_weights.green; + var newDiv = $('
').css('color', 'green').html('I have a weight of ' + weight); + $('#js-weights').append(newDiv); + } + }; +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/examples/examples/js_example/js/js_example_accordion.js b/sites/all/modules/examples/examples/js_example/js/js_example_accordion.js new file mode 100644 index 000000000..4cf19df91 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js/js_example_accordion.js @@ -0,0 +1,13 @@ +/** + * @file + * Contains js for the accordion example. + */ + +(function ($) { + + 'use strict'; + + $(function () { + $('#accordion').accordion(); + }); +})(jQuery); diff --git a/sites/all/modules/examples/examples/js_example/js/purple.js b/sites/all/modules/examples/examples/js_example/js/purple.js new file mode 100755 index 000000000..5652ad341 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js/purple.js @@ -0,0 +1,20 @@ +/** + * @file + * Contains the definition of the behaviour jsTestPurpleWeight. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the JS test behavior to weight div. + */ + Drupal.behaviors.jsTestPurpleWeight = { + attach: function (context, settings) { + var weight = drupalSettings.js_example.js_weights.purple; + var newDiv = $('
').css('color', 'purple').html('I have a weight of ' + weight); + $('#js-weights').append(newDiv); + } + }; +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/examples/examples/js_example/js/red.js b/sites/all/modules/examples/examples/js_example/js/red.js new file mode 100755 index 000000000..5dfc4274d --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js/red.js @@ -0,0 +1,20 @@ +/** + * @file + * Contains the definition of the behaviour jsTestRedWeight. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the JS test behavior to weight div. + */ + Drupal.behaviors.jsTestRedWeight = { + attach: function (context, settings) { + var weight = drupalSettings.js_example.js_weights.red; + var newDiv = $('
').css('color', 'red').html('I have a weight of ' + weight); + $('#js-weights').append(newDiv); + } + }; +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/examples/examples/js_example/js_example.info.yml b/sites/all/modules/examples/examples/js_example/js_example.info.yml new file mode 100644 index 000000000..4ca2d5e4c --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js_example.info.yml @@ -0,0 +1,14 @@ +name: JavaScript Example +description: Demonstrates JavaScript features in Drupal 8. +type: module +package: 'Example modules' +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/js_example/js_example.libraries.yml b/sites/all/modules/examples/examples/js_example/js_example.libraries.yml new file mode 100644 index 000000000..496f74808 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js_example.libraries.yml @@ -0,0 +1,28 @@ +# A simple script using jquery ui accordion. +js_example.accordion: + js: + js/js_example_accordion.js: {} + # Build up our dependencies for this page as a library. Our accordion script + # needs jquery.ui.accordion. You can find the core scripts under core/assets/. + dependencies: + - core/jquery.ui.accordion + +# Show how weights work. +js_example.weights: + # Add css file. + css: + component: + css/jsweights.css: {} + # Add js files. + js: + # weight is used as example attribute, but please avoid it and instead use + # dependencies for order loading. + js/red.js: { weight: -4 } + js/blue.js: { weight: -2 } + js/green.js: { weight: -1 } + js/brown.js: { weight: -2 } + js/black.js: { weight: -1 } + js/purple.js: { weight: -5 } + dependencies: + - core/jquery + - core/drupalSettings diff --git a/sites/all/modules/examples/examples/js_example/js_example.links.menu.yml b/sites/all/modules/examples/examples/js_example/js_example.links.menu.yml new file mode 100644 index 000000000..d2e7b0390 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js_example.links.menu.yml @@ -0,0 +1,18 @@ +# Define default links for this module. +js_example.info: + title: JavaScript Example + description: Some information about the JavaScript example. + route_name: js_example.info + expanded: TRUE + +js_example.weights: + title: JavaScript weighting in action + description: Show list weighting through JavaScript. + route_name: js_example.weights + parent: js_example.info + +js_example.accordion: + title: jQuery UI accordion + description: jQuery UI accordion demo. + route_name: js_example.accordion + parent: js_example.info diff --git a/sites/all/modules/examples/examples/js_example/js_example.module b/sites/all/modules/examples/examples/js_example/js_example.module new file mode 100755 index 000000000..e35b3680f --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js_example.module @@ -0,0 +1,37 @@ + [ + 'template' => 'accordion', + 'variables' => ['title' => NULL], + ], + ]; +} + +/** + * @} End of "defgroup js_example". + */ diff --git a/sites/all/modules/examples/examples/js_example/js_example.routing.yml b/sites/all/modules/examples/examples/js_example/js_example.routing.yml new file mode 100755 index 000000000..c25ecf3a6 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/js_example.routing.yml @@ -0,0 +1,23 @@ +js_example.info: + path: '/examples/js-example' + defaults: + _title: 'JavaScript Example Information' + _controller: '\Drupal\js_example\Controller\JsExampleController::description' + requirements: + _permission: 'access content' + +js_example.weights: + path: '/examples/js-example/weights' + defaults: + _title: 'JS Example: see weighting in action' + _controller: '\Drupal\js_example\Controller\JsExampleController::getJsWeightImplementation' + requirements: + _permission: 'access content' + +js_example.accordion: + path: '/examples/js-example/accordion' + defaults: + _title: 'JS Example: jQuery UI accordion' + _controller: '\Drupal\js_example\Controller\JsExampleController::getJsAccordionImplementation' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/js_example/src/Controller/JsExampleController.php b/sites/all/modules/examples/examples/js_example/src/Controller/JsExampleController.php new file mode 100755 index 000000000..ccdafcba8 --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/src/Controller/JsExampleController.php @@ -0,0 +1,111 @@ +weight array to drupalSettings, which is where Drupal + * passes data out to JavaScript. + * + * Each of the color scripts (red.js, blue.js, etc) uses jQuery to find our + * DIV, and then add some content to it. The order in which the color scripts + * execute will end up being the order of the content. + * + * The 'weight' atttribute in libraries yml file determines the order in which + * a script is output to the page. To see this in action: + * - Uncheck the 'Aggregate Javascript files' setting at: + * admin/config/development/performance. + * - Load the page: examples/js_example/weights. Examine the page source. + * You will see that the color js scripts have been added in the + * element in weight order. + * + * To test further, change a weight in the $weights array below and in library + * yml file, then rebuild cache and reload examples/js_example/weights. + * Examine the new source to see the reordering. + * + * @return array + * A renderable array. + */ + public function getJsWeightImplementation() { + // Create an array of items with random-ish weight values. + $weights = [ + 'red' => -4, + 'blue' => -2, + 'green' => -1, + 'brown' => -2, + 'black' => -1, + 'purple' => -5, + ]; + + // Start building the content. + $build = []; + // Main container DIV. We give it a unique ID so that the JavaScript can + // find it using jQuery. + $build['content'] = [ + '#markup' => '
', + ]; + // Attach library containing css and js files. + $build['#attached']['library'][] = 'js_example/js_example.weights'; + // Attach the weights array to our JavaScript settings. This allows the + // color scripts we just attached to discover their weight values, by + // accessing drupalSettings.js_example.js_weights.*color*. The color scripts + // only use this information for display to the user. + $build['#attached']['drupalSettings']['js_example']['js_weights'] = $weights; + + return $build; + } + + /** + * Accordion page implementation. + * + * We're allowing a twig template to define our content in this case, + * which isn't normally how things work, but it's easier to demonstrate + * the JavaScript this way. + * + * @return array + * A renderable array. + */ + public function getJsAccordionImplementation() { + $title = t('Click sections to expand or collapse:'); + // Build using our theme. This gives us content, which is not a good + // practice, but which allows us to demonstrate adding JavaScript here. + $build['myelement'] = [ + '#theme' => 'js_example_accordion', + '#title' => $title, + ]; + // Add our script. It is tiny, but this demonstrates how to add it. We pass + // our module name followed by the internal library name declared in + // libraries yml file. + $build['myelement']['#attached']['library'][] = 'js_example/js_example.accordion'; + // Return the renderable array. + return $build; + } + +} diff --git a/sites/all/modules/examples/examples/js_example/templates/accordion.html.twig b/sites/all/modules/examples/examples/js_example/templates/accordion.html.twig new file mode 100755 index 000000000..6c54e950c --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/templates/accordion.html.twig @@ -0,0 +1,60 @@ +{# + +/** + * @file + * Template file for js_example module. + */ +#} + +
+

{{ title }}

+
+

Section 1

+
+

+ Mauris mauris ante, blandit et, ultrices a, suscipit eget, quam. Integer + ut neque. Vivamus nisi metus, molestie vel, gravida in, condimentum sit + amet, nunc. Nam a nibh. Donec suscipit eros. Nam mi. Proin viverra leo ut + odio. Curabitur malesuada. Vestibulum a velit eu ante scelerisque vulputate. +

+
+

Section 2

+
+

+ Sed non urna. Donec et ante. Phasellus eu ligula. Vestibulum sit amet + purus. Vivamus hendrerit, dolor at aliquet laoreet, mauris turpis porttitor + velit, faucibus interdum tellus libero ac justo. Vivamus non quam. In + suscipit faucibus urna. +

+
+

Section 3

+
+

+ Nam enim risus, molestie et, porta ac, aliquam ac, risus. Quisque lobortis. + Phasellus pellentesque purus in massa. Aenean in pede. Phasellus ac libero + ac tellus pellentesque semper. Sed ac felis. Sed commodo, magna quis + lacinia ornare, quam ante aliquam nisi, eu iaculis leo purus venenatis dui. +

+
    +
  • List item one
  • +
  • List item two
  • +
  • List item three
  • +
+
+

Section 4

+
+

+ Cras dictum. Pellentesque habitant morbi tristique senectus et netus + et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in + faucibus orci luctus et ultrices posuere cubilia Curae; Aenean lacinia + mauris vel est. +

+

+ Suspendisse eu nisl. Nullam ut libero. Integer dignissim consequat lectus. + Class aptent taciti sociosqu ad litora torquent per conubia nostra, per + inceptos himenaeos. +

+
+
+ +
diff --git a/sites/all/modules/examples/examples/js_example/templates/description.html.twig b/sites/all/modules/examples/examples/js_example/templates/description.html.twig new file mode 100644 index 000000000..425ee7fee --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/templates/description.html.twig @@ -0,0 +1,23 @@ +{# + +Description text for the JavaScript Example. + +#} + +{% trans %} + +

Drupal includes jQuery and jQuery UI.

+ +

We have two examples of using these:

+ +
    +
  1. +

    An accordion-style section reveal effect: This demonstrates calling a jQuery + UI function using Drupal rendering system. +

  2. +
  3. +

    Sorting according to numeric weight: This demonstrates attaching your own + JavaScript code to individual page elements using Drupal rendering system.

    +
  4. +
+{% endtrans %} diff --git a/sites/all/modules/examples/examples/js_example/tests/src/Functional/JsExampleTest.php b/sites/all/modules/examples/examples/js_example/tests/src/Functional/JsExampleTest.php new file mode 100644 index 000000000..1e3361ced --- /dev/null +++ b/sites/all/modules/examples/examples/js_example/tests/src/Functional/JsExampleTest.php @@ -0,0 +1,40 @@ +assertSession(); + + $paths = [ + 'examples/js-example', + 'examples/js-example/weights', + 'examples/js-example/accordion', + ]; + foreach ($paths as $path) { + $this->drupalGet($path); + $assert->statusCodeEquals(200); + } + } + +} diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.basic_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.basic_content_type.default.yml new file mode 100644 index 000000000..b6d281c53 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.basic_content_type.default.yml @@ -0,0 +1,64 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.basic_content_type.body + - node.type.basic_content_type + module: + - path + - text + enforced: + module: + - node_type_example +id: node.basic_content_type.default +targetEntityType: node +bundle: basic_content_type +mode: default +content: + title: + type: string_textfield + weight: 0 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 1 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + third_party_settings: { } + created: + type: datetime_timestamp + weight: 2 + settings: { } + third_party_settings: { } + promote: + type: boolean_checkbox + weight: 3 + settings: + display_label: true + third_party_settings: { } + sticky: + type: boolean_checkbox + weight: 4 + settings: + display_label: true + third_party_settings: { } + path: + type: path + weight: 5 + settings: { } + third_party_settings: { } + body: + type: text_textarea_with_summary + weight: 6 + settings: + rows: 9 + summary_rows: 3 + placeholder: '' + third_party_settings: { } +hidden: { } +third_party_settings: { } diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.locked_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.locked_content_type.default.yml new file mode 100644 index 000000000..29050d4a2 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_form_display.node.locked_content_type.default.yml @@ -0,0 +1,64 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.locked_content_type.body + - node.type.locked_content_type + module: + - path + - text + enforced: + module: + - node_type_example +id: node.locked_content_type.default +targetEntityType: node +bundle: locked_content_type +mode: default +content: + title: + type: string_textfield + weight: 0 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 1 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + third_party_settings: { } + created: + type: datetime_timestamp + weight: 2 + settings: { } + third_party_settings: { } + promote: + type: boolean_checkbox + weight: 3 + settings: + display_label: true + third_party_settings: { } + sticky: + type: boolean_checkbox + weight: 4 + settings: + display_label: true + third_party_settings: { } + path: + type: path + weight: 5 + settings: { } + third_party_settings: { } + body: + type: text_textarea_with_summary + weight: 6 + settings: + rows: 9 + summary_rows: 3 + placeholder: '' + third_party_settings: { } +hidden: { } +third_party_settings: { } diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.basic_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.basic_content_type.default.yml new file mode 100644 index 000000000..e5977fc90 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.basic_content_type.default.yml @@ -0,0 +1,26 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.basic_content_type.body + - node.type.basic_content_type + module: + - text + - user + enforced: + module: + - node_type_example +id: node.basic_content_type.default +targetEntityType: node +bundle: basic_content_type +mode: default +content: + body: + label: hidden + type: text_default + weight: 101 + settings: { } + third_party_settings: { } + links: + weight: 100 +hidden: { } diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.locked_content_type.default.yml b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.locked_content_type.default.yml new file mode 100644 index 000000000..b0e6b6a9a --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/core.entity_view_display.node.locked_content_type.default.yml @@ -0,0 +1,26 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.locked_content_type.body + - node.type.locked_content_type + module: + - text + - user + enforced: + module: + - node_type_example +id: node.locked_content_type.default +targetEntityType: node +bundle: locked_content_type +mode: default +content: + body: + label: hidden + type: text_default + weight: 101 + settings: { } + third_party_settings: { } + links: + weight: 100 +hidden: { } diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.basic_content_type.body.yml b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.basic_content_type.body.yml new file mode 100644 index 000000000..d3d7f68b0 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.basic_content_type.body.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.body + - node.type.basic_content_type + module: + - text + enforced: + module: + - node_type_example +id: node.basic_content_type.body +field_name: body +entity_type: node +bundle: basic_content_type +label: Body +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + display_summary: true +third_party_settings: { } +field_type: text_with_summary diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.locked_content_type.body.yml b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.locked_content_type.body.yml new file mode 100644 index 000000000..4962e72b2 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/field.field.node.locked_content_type.body.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.body + - node.type.locked_content_type + module: + - text + enforced: + module: + - node_type_example +id: node.locked_content_type.body +field_name: body +entity_type: node +bundle: locked_content_type +label: Body +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + display_summary: true +third_party_settings: { } +field_type: text_with_summary diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/node.type.basic_content_type.yml b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.basic_content_type.yml new file mode 100644 index 000000000..8b8f33f6b --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.basic_content_type.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - node_type_example +name: 'Example: Basic Content Type' +type: basic_content_type +description: 'This is a very basic content type.' +help: '' +new_revision: false +preview_mode: 1 +display_submitted: true +third_party_settings: { } diff --git a/sites/all/modules/examples/examples/node_type_example/config/install/node.type.locked_content_type.yml b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.locked_content_type.yml new file mode 100644 index 000000000..fb506be1f --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/config/install/node.type.locked_content_type.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - node_type_example +name: 'Example: Locked Content Type' +type: locked_content_type +description: 'Demo of a locked content type. Locking must be done in code. You must uninstall this module before you can delete the content type.' +help: '' +new_revision: false +preview_mode: 1 +display_submitted: true +third_party_settings: { } diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.info.yml b/sites/all/modules/examples/examples/node_type_example/node_type_example.info.yml new file mode 100644 index 000000000..77e5e588f --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.info.yml @@ -0,0 +1,16 @@ +name: Node Type Example +description: Demonstrates implementing a node content type in a module. +package: Example modules +type: module +# core: 8.x +dependencies: + - drupal:node + - drupal:path + - drupal:text + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.install b/sites/all/modules/examples/examples/node_type_example/node_type_example.install new file mode 100644 index 000000000..c408989e4 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.install @@ -0,0 +1,43 @@ +get('node.type.locked'); + $locked['locked_content_type'] = 'locked_content_type'; + Drupal::state()->set('node.type.locked', $locked); +} + +/** + * Implements hook_uninstall(). + * + * Our content types will live on in the Drupal installation, even after this + * module is uninstalled. This is a good thing, since it allows the user to + * make decisions about their fate. Therefore we should give the user the + * option of deleting them. + * + * Since we told Drupal that our locked_content_type is locked, we now have + * to tell it to unlock. + * + * @ingroup node_type_example + */ +function node_type_example_uninstall() { + // Allow locked_content_type to be deleted. + $locked = Drupal::state()->get('node.type.locked'); + unset($locked['locked_content_type']); + Drupal::state()->set('node.type.locked', $locked); +} diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.links.menu.yml b/sites/all/modules/examples/examples/node_type_example/node_type_example.links.menu.yml new file mode 100644 index 000000000..476f03931 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.links.menu.yml @@ -0,0 +1,3 @@ +node_type_example.description: + title: Config Node Type Example + route_name: config_node_type_example.description diff --git a/sites/all/modules/examples/examples/node_type_example/node_type_example.module b/sites/all/modules/examples/examples/node_type_example/node_type_example.module new file mode 100644 index 000000000..22c0ce626 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/node_type_example.module @@ -0,0 +1,44 @@ +Config Node Type Example is a basic example of defining a content type through configuration YAML files.

+

In this example we create two content types for Drupal 8, using only YAML files. Well, mostly only YAML files... + One of our content types is locked, so the user can't delete it while the module is installed. For this we need + a very tiny amount of support code.

+

You can observe these content types on the content type admin page.

+

The simplest way to author the per-type YAML files is to create the content types within Drupal and then take the + YAML files from the configuration directory. Like this:

+
    +
  • Install Drupal 8.
  • +
  • Create a new content type at {{ add_types }}. Let's call it 'Nifty Content Type'.
  • +
  • Export the configuration from admin/config/development/configuration. Specific steps depending + on needs, and decisions made during Drupal 8 beta. You'll see a file called + node.type.nifty_content_type.yml.
  • +
  • Copy or move that file to your module's config/install directory, along with associated field + and form yml files.
  • +
  • Remove the uuid information from the configuration files.
  • +
+

You can see some of these YAML files in this module's config/install directory.

+

If you want to lock a content type created in this way, you'll have to implement hook_install() and + hook_uninstall(). In hook_install(), you'll set the content type to be locked. + In hook_uninstall() you'll set the content type to be unlocked.

+

Content types created in this way will remain available after the user has uninstalled the module. + If you were to fail to set the content type as unlocked, the user would not be able to delete it.

+

This example is based on this change notification: https://drupal.org/node/2029519

+ +{% endtrans %} diff --git a/sites/all/modules/examples/examples/node_type_example/tests/src/Functional/NodeTypeExampleTest.php b/sites/all/modules/examples/examples/node_type_example/tests/src/Functional/NodeTypeExampleTest.php new file mode 100644 index 000000000..fea8b4663 --- /dev/null +++ b/sites/all/modules/examples/examples/node_type_example/tests/src/Functional/NodeTypeExampleTest.php @@ -0,0 +1,175 @@ + link relationships to check for. + * - The key is the path to the page where our link should appear. + * - The value is the link that should appear on that page. + */ + protected function providerMenuLinks() { + return [ + '' => '/examples/node-type-example', + ]; + } + + /** + * Verify and validate that default menu links were loaded for this module. + */ + public function testNodeTypeExample() { + $assert = $this->assertSession(); + // Test that our page loads. + $this->drupalGet('/examples/node-type-example'); + $assert->statusCodeEquals(200); + + // Test that our menu links were created. + $links = $this->providerMenuLinks(); + foreach ($links as $page => $path) { + $this->drupalGet($page); + $assert->linkByHrefExists($path); + } + } + + /** + * Test our new content types. + * + * Tests for the following: + * + * - That our content types appear in the user interface. + * - That our unlocked content type is unlocked. + * - That our locked content type is locked. + * - That we can create content using the user interface. + * - That our created content does appear in the database. + */ + public function testNodeTypes() { + $assert = $this->assertSession(); + + // Log in an admin user. + $admin_user = $this->drupalCreateUser(['administer content types']); + $this->drupalLogin($admin_user); + + // Get a list of content types. + $this->drupalGet('/admin/structure/types'); + // Verify that these content types show up in the user interface. + $assert->pageTextContains('Example: Basic Content Type', 'Basic content type found.'); + $assert->pageTextContains('Example: Locked Content Type', 'Locked content type found.'); + + // Check for the locked status of our content types. + // $nodeType will be of type Drupal\node\NodeTypeInterface. + $node_type = NodeType::load('basic_content_type'); + $this->assertTrue($node_type, 'basic_content_type exists.'); + if ($node_type) { + $this->assertFalse($node_type->isLocked(), 'basic_content_type is not locked.'); + } + $node_type = NodeType::load('locked_content_type'); + $this->assertTrue($node_type, 'locked_content_type exists.'); + if ($node_type) { + $this->assertEquals('locked_content_type', $node_type->isLocked()); + } + + // Log in a content creator. + $creator_user = $this->drupalCreateUser(['create basic_content_type content']); + $this->drupalLogin($creator_user); + + // Create a node. + $edit = []; + $edit['title[0][value]'] = $this->randomMachineName(8); + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->drupalPostForm('/node/add/basic_content_type', $edit, 'Save'); + + // Check that the Basic page has been created. + $assert->pageTextContains(t('@post @title has been created.', [ + '@post' => 'Example: Basic Content Type', + '@title' => $edit['title[0][value]'], + ])); + + // Check that the node exists in the database. + $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); + $this->assertTrue($node, 'Node found in database.'); + } + + /** + * Test that all fields are displayed when content is created. + */ + public function testNodeCreation() { + // Login content creator. + $this->drupalLogin( + $this->drupalCreateUser([ + 'create basic_content_type content', + 'create locked_content_type content', + ]) + ); + + // Create random strings to insert data into fields. + $title = 'Test title.'; + $body = 'Test body.'; + $edit = []; + $edit['title[0][value]'] = $title; + $edit['body[0][value]'] = $body; + + // Create a basic_content_type content. + $this->drupalPostForm('/node/add/basic_content_type', $edit, 'Save'); + // Verify all fields and data of created content is shown. + $this->assertText($title); + $this->assertText($body); + + // Create a locked_content_type content. + $this->drupalPostForm('/node/add/locked_content_type', $edit, 'Save'); + // Verify all fields and data of created content is shown. + $this->assertText($title); + $this->assertText($body); + } + + + /** + * Verify that user can uninstall and then reinstall node_type_example. + */ + public function testUninstallReinstall() { + $session = $this->assertSession(); + + // Uninstalling module. + $module_installer = $this->container->get('module_installer'); + $module_installer->uninstall(['node_type_example']); + $this->drupalGet('examples/node-type-example'); + $session->statusCodeEquals(404); + + // Re-installing module. + $module_installer->install(['node_type_example']); + $this->drupalGet('examples/node-type-example'); + $session->statusCodeEquals(200); + } + +} diff --git a/sites/all/modules/examples/examples/page_example/page_example.info.yml b/sites/all/modules/examples/examples/page_example/page_example.info.yml new file mode 100644 index 000000000..a560387e1 --- /dev/null +++ b/sites/all/modules/examples/examples/page_example/page_example.info.yml @@ -0,0 +1,14 @@ +name: Page Example +type: module +description: 'Demonstrates how to display a page at a given URL.' +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/page_example/page_example.links.menu.yml b/sites/all/modules/examples/examples/page_example/page_example.links.menu.yml new file mode 100644 index 000000000..db95585a3 --- /dev/null +++ b/sites/all/modules/examples/examples/page_example/page_example.links.menu.yml @@ -0,0 +1,12 @@ +page_example.description: + title: Page Example + route_name: page_example_description + expanded: TRUE + +page_example.simple: + title: Simple - no arguments + route_name: page_example_simple + parent: page_example.description + +# We can't define a menu link for the page_example_arguments route, because it +# requires path arguments. diff --git a/sites/all/modules/examples/examples/page_example/page_example.module b/sites/all/modules/examples/examples/page_example/page_example.module new file mode 100644 index 000000000..a1d0ab9d6 --- /dev/null +++ b/sites/all/modules/examples/examples/page_example/page_example.module @@ -0,0 +1,60 @@ + '

' . $this->t('Simple page: The quick brown fox jumps over the lazy dog.') . '

', + ]; + } + + /** + * A more complex _controller callback that takes arguments. + * + * This callback is mapped to the path + * 'examples/page-example/arguments/{first}/{second}'. + * + * The arguments in brackets are passed to this callback from the page URL. + * The placeholder names "first" and "second" can have any value but should + * match the callback method variable names; i.e. $first and $second. + * + * This function also demonstrates a more complex render array in the returned + * values. Instead of rendering the HTML with theme('item_list'), content is + * left un-rendered, and the theme function name is set using #theme. This + * content will now be rendered as late as possible, giving more parts of the + * system a chance to change it if necessary. + * + * Consult @link http://drupal.org/node/930760 Render Arrays documentation + * @endlink for details. + * + * @param string $first + * A string to use, should be a number. + * @param string $second + * Another string to use, should be a number. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * If the parameters are invalid. + */ + public function arguments($first, $second) { + // Make sure you don't trust the URL to be safe! Always check for exploits. + if (!is_numeric($first) || !is_numeric($second)) { + // We will just show a standard "access denied" page in this case. + throw new AccessDeniedHttpException(); + } + + $list[] = $this->t("First number was @number.", ['@number' => $first]); + $list[] = $this->t("Second number was @number.", ['@number' => $second]); + $list[] = $this->t('The total was @number.', ['@number' => $first + $second]); + + $render_array['page_example_arguments'] = [ + // The theme function to apply to the #items. + '#theme' => 'item_list', + // The list itself. + '#items' => $list, + '#title' => $this->t('Argument Information'), + ]; + return $render_array; + } + +} diff --git a/sites/all/modules/examples/examples/page_example/templates/description.html.twig b/sites/all/modules/examples/examples/page_example/templates/description.html.twig new file mode 100644 index 000000000..e344cb72d --- /dev/null +++ b/sites/all/modules/examples/examples/page_example/templates/description.html.twig @@ -0,0 +1,15 @@ +{# +/** + * @file + * Contains the text of the page_example explanation page + */ +#} + +{% set page_example_simple = path('page_example_simple') %} +{% set page_example_arguments = path('page_example_arguments', {'first': 23, 'second': 56}) %} + +{% trans %} +

The Page example module provides two pages, "simple" and "arguments".

+

The simple page just returns a renderable array for display.

+

The arguments page takes two arguments and displays them, as in {{ page_example_arguments }}

+{% endtrans %} diff --git a/sites/all/modules/examples/examples/page_example/tests/src/Functional/PageExampleTest.php b/sites/all/modules/examples/examples/page_example/tests/src/Functional/PageExampleTest.php new file mode 100644 index 000000000..3e574f292 --- /dev/null +++ b/sites/all/modules/examples/examples/page_example/tests/src/Functional/PageExampleTest.php @@ -0,0 +1,176 @@ +drupalGet($url); + $this->assertSession()->statusCodeEquals(403); + } + + /** + * Data provider for testing menu links. + * + * @return array + * + * Array of page -> link relationships to check for, with the permissions + * required to access them: + * - Permission machine name. Empty string means no login. + * - Array of link information: + * - Key is path to the page where the link should appear. + * - Value is the link that should appear on the page. + */ + public function providerMenuLinks() { + return [ + [ + '', + ['' => '/examples/page-example'], + ], + [ + 'access simple page', + ['/examples/page-example' => '/examples/page-example/simple'], + ], + ]; + } + + /** + * Verify and validate that default menu links were loaded for this module. + * + * @dataProvider providerMenuLinks + */ + public function testPageExampleLinks($permission, $links) { + if ($permission) { + $user = $this->drupalCreateUser([$permission]); + $this->drupalLogin($user); + } + foreach ($links as $page => $path) { + $this->drupalGet($page); + $this->assertSession()->linkByHrefExists($path); + } + if ($permission) { + $this->drupalLogout(); + } + } + + /** + * Main test. + * + * Login user, create an example node, and test page functionality through + * the admin and user interfaces. + */ + public function testPageExample() { + $assert_session = $this->assertSession(); + // Verify that anonymous user can't access the pages created by + // page_example module. + $this->pageExampleVerifyNoAccess('examples/page-example/simple'); + $this->pageExampleVerifyNoAccess('examples/page-example/arguments/1/2'); + + // Create a regular user and login. + $this->webUser = $this->drupalCreateUser(); + $this->drupalLogin($this->webUser); + + // Verify that regular user can't access the pages created by + // page_example module. + $this->pageExampleVerifyNoAccess('examples/page-example/simple'); + $this->pageExampleVerifyNoAccess('examples/page-example/arguments/1/2'); + + // Create a user with permissions to access 'simple' page and login. + $this->webUser = $this->drupalCreateUser(['access simple page']); + $this->drupalLogin($this->webUser); + + // Verify that user can access simple content. + $this->drupalGet('/examples/page-example/simple'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('The quick brown fox jumps over the lazy dog.'); + + // Check if user can't access arguments page. + $this->pageExampleVerifyNoAccess('examples/page-example/arguments/1/2'); + + // Create a user with permissions to access 'simple' page and login. + $this->webUser = $this->drupalCreateUser(['access arguments page']); + $this->drupalLogin($this->webUser); + + // Verify that user can access arguments content. + $first = self::randomNumber(3); + $second = self::randomNumber(3); + $this->drupalGet('/examples/page-example/arguments/' . $first . '/' . $second); + $assert_session->statusCodeEquals(200); + // Verify argument usage. + $assert_session->pageTextContains(t('First number was @number.', ['@number' => $first])); + $assert_session->pageTextContains(t('Second number was @number.', ['@number' => $second])); + $assert_session->pageTextContains(t('The total was @number.', ['@number' => $first + $second])); + + // Verify incomplete argument call to arguments content. + $this->drupalGet('/examples/page-example/arguments/' . $first . '/'); + $assert_session->statusCodeEquals(404); + + // Verify 403 for invalid second argument. + $this->drupalGet('/examples/page-example/arguments/' . $first . '/non-numeric-argument'); + $assert_session->statusCodeEquals(403); + + // Verify 403 for invalid first argument. + $this->drupalGet('/examples/page-example/arguments/non-numeric-argument/' . $second); + $assert_session->statusCodeEquals(403); + + // Check if user can't access simple page. + $this->pageExampleVerifyNoAccess('examples/page-example/simple'); + } + +} diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.info.yml b/sites/all/modules/examples/examples/pager_example/pager_example.info.yml new file mode 100644 index 000000000..1cbc11116 --- /dev/null +++ b/sites/all/modules/examples/examples/pager_example/pager_example.info.yml @@ -0,0 +1,14 @@ +name: Pager Example +type: module +description: Demonstrates how to use table and render page elements to create paginated tables. +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.links.menu.yml b/sites/all/modules/examples/examples/pager_example/pager_example.links.menu.yml new file mode 100644 index 000000000..c34a73d32 --- /dev/null +++ b/sites/all/modules/examples/examples/pager_example/pager_example.links.menu.yml @@ -0,0 +1,3 @@ +pager_example.page: + title: Pager Example + route_name: pager_example.page diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.module b/sites/all/modules/examples/examples/pager_example/pager_example.module new file mode 100644 index 000000000..f9591fbf8 --- /dev/null +++ b/sites/all/modules/examples/examples/pager_example/pager_example.module @@ -0,0 +1,33 @@ + [ + 'template' => 'description', + 'variables' => [], + ], + ]; +} + +/** + * @} End of "defgroup pager_example". + */ diff --git a/sites/all/modules/examples/examples/pager_example/pager_example.routing.yml b/sites/all/modules/examples/examples/pager_example/pager_example.routing.yml new file mode 100644 index 000000000..fc255cbf9 --- /dev/null +++ b/sites/all/modules/examples/examples/pager_example/pager_example.routing.yml @@ -0,0 +1,6 @@ +pager_example.page: + path: 'examples/pager-example' + defaults: + _controller: '\Drupal\pager_example\Controller\PagerExamplePage::getContent' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/pager_example/src/Controller/PagerExamplePage.php b/sites/all/modules/examples/examples/pager_example/src/Controller/PagerExamplePage.php new file mode 100644 index 000000000..1546f76e7 --- /dev/null +++ b/sites/all/modules/examples/examples/pager_example/src/Controller/PagerExamplePage.php @@ -0,0 +1,175 @@ +nodeStorage = $node_storage; + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + $controller = new static( + $container->get('entity_type.manager')->getStorage('node'), + $container->get('current_user') + ); + $controller->setStringTranslation($container->get('string_translation')); + return $controller; + } + + /** + * Content callback for the pager_example.page route. + */ + public function getContent() { + // First we'll tell the user what's going on. This content can be found + // in the twig template file: templates/description.html.twig. + // @todo: Set up links to create nodes and point to devel module. + $build = [ + 'description' => [ + '#theme' => 'pager_example_description', + '#description' => 'foo', + '#attributes' => [], + ], + ]; + + // We need to count the number of nodes so that we can tell the user to add + // some if there aren't any. + $query = $this->nodeStorage->getQuery() + ->addTag('node_access') + ->count(); + + // The node_access tag does not trigger a check on whether a user has the + // ability to view unpublished content. The 'bypass node access' permission + // is really more than we need. But, there is no separate permission for + // viewing unpublished content. There is a permission to 'view own + // unpublished content', but we don't have a good way of using that in this + // query. So, unfortunately this query will incorrectly eliminate even those + // unpublished nodes that the user may, in fact, be allowed to view. + if (!$this->currentUser->hasPermission('bypass node access')) { + $query->condition('status', 1); + } + $count_nodes = $query->execute(); + + if ($count_nodes == 0) { + if ($this->currentUser->hasPermission('create page content')) { + $build['no-nodes'] = [ + '#markup' => $this->t('There are no nodes to display. + Please create a node.', + [ + ':url' => Url::fromRoute('node.add', ['node_type' => 'page'])->toString(), + ] + ), + ]; + } + else { + $build['no-nodes'] = [ + '#markup' => $this->t('There are no nodes to display.'), + ]; + } + + // Ensure that Drupal clears the cache when nodes have been published, + // unpublished, added or deleted; and when user permissions change. + $build['#cache']['tags'][] = 'node_list'; + $build['#cache']['contexts'][] = 'user.permissions'; + return $build; + } + + // Now we want to get our tabular data. We select nodes from node storage + // limited by 2 per page and sort by nid DESC because we want to show newest + // node first. Additionally, we check that the user has permission to + // view the node. + $query = $this->nodeStorage->getQuery() + ->sort('nid', 'DESC') + ->addTag('node_access') + ->pager(2); + + // The node_access tag does not trigger a check on whether a user has the + // ability to view unpublished content. The 'bypass node access' permission + // is really more than we need. But, there is no separate permission for + // viewing unpublished content. There is a permission to 'view own + // unpublished content', but we don't have a good way of using that in this + // query. So, unfortunately this query will incorrectly eliminate even those + // unpublished nodes that the user may, in fact, be allowed to view. + if (!$this->currentUser->hasPermission('bypass node access')) { + $query->condition('status', 1); + } + $entity_ids = $query->execute(); + + $nodes = $this->nodeStorage->loadMultiple($entity_ids); + + // We are going to output the results in a table so we set up the rows. + $rows = []; + foreach ($nodes as $node) { + // There are certain things (besides unpublished nodes) that the + // node_access tag won't prevent from being seen. The only way to get at + // those is by explicitly checking for (view) access on a node-by-node + // basis. In order to prevent the pager from looking strange, we will + // "mask" these nodes that should not be accessible. If we don't do this + // masking, it's possible that we'd have lots of pages that don't show any + // content. + $rows[] = [ + 'nid' => $node->access('view') ? $node->id() : t('XXXXXX'), + 'title' => $node->access('view') ? $node->getTitle() : t('Redacted'), + ]; + } + + // Build a render array which will be themed as a table with a pager. + $build['pager_example'] = [ + '#rows' => $rows, + '#header' => [t('NID'), t('Title')], + '#type' => 'table', + '#empty' => t('No content available.'), + ]; + $build['pager'] = [ + '#type' => 'pager', + '#weight' => 10, + ]; + + // Ensure that Drupal clears the cache when nodes have been published, + // unpublished, added or deleted; and when user permissions change. + $build['#cache']['tags'][] = 'node_list'; + $build['#cache']['contexts'][] = 'user.permissions'; + + return $build; + } + +} diff --git a/sites/all/modules/examples/examples/pager_example/templates/description.html.twig b/sites/all/modules/examples/examples/pager_example/templates/description.html.twig new file mode 100644 index 000000000..8deb25a16 --- /dev/null +++ b/sites/all/modules/examples/examples/pager_example/templates/description.html.twig @@ -0,0 +1,20 @@ +{# +/** + * @file + * Contains the text of the pager_example explanation page + */ +#} + +
+{% trans %} +

The Pager Example shows you how to create a paginated table. It uses + an Entity Query to retrieve nodes and the query checks that + the user has access to the nodes. You may test the access checking + feature by unpublishing some of your nodes and then viewing the + example as a user that is not allowed to see unpublished content.

+

In order to see this in action, make sure there are some nodes present in + your site. You can use the devel module to add some if needed, or just + create them.

+

The table on this page will show you two nodes per page.

+{% endtrans %} +
diff --git a/sites/all/modules/examples/examples/pager_example/tests/src/Functional/PagerExampleTest.php b/sites/all/modules/examples/examples/pager_example/tests/src/Functional/PagerExampleTest.php new file mode 100644 index 000000000..b4a2b7e9a --- /dev/null +++ b/sites/all/modules/examples/examples/pager_example/tests/src/Functional/PagerExampleTest.php @@ -0,0 +1,68 @@ +drupalCreateUser(); + $this->drupalLogin($normalUser); + } + + /** + * Confirms nodes paging works correctly on page "pager_example". + */ + public function testPagerExamplePage() { + $assert = $this->assertSession(); + + $nodes = []; + $nodes[] = $this->drupalCreateNode(); + + $this->drupalGet('examples/pager-example'); + $assert->linkNotExists('Next'); + $assert->linkNotExists('Previous'); + + // Create 5 new nodes. + for ($i = 1; $i <= 5; $i++) { + $nodes[] = $this->drupalCreateNode([ + 'title' => "Node number $i", + ]); + } + + // The pager pages are cached, so flush to see the 5 more nodes. + drupal_flush_all_caches(); + + // Check 'Next' link on first page. + $this->drupalGet('examples/pager-example'); + $assert->statusCodeEquals(200); + $assert->linkByHrefExists('?page=1'); + $assert->pageTextContains($nodes[5]->getTitle()); + + // Check the last page. + $this->drupalGet('examples/pager-example', ['query' => ['page' => 2]]); + $assert->statusCodeEquals(200); + $assert->linkNotExists('Next'); + $assert->linkByHrefExists('?page=1'); + $assert->pageTextContains($nodes[1]->getTitle()); + } + +} diff --git a/sites/all/modules/examples/examples/phpcs.xml.dist b/sites/all/modules/examples/examples/phpcs.xml.dist new file mode 100644 index 000000000..0bb25b9be --- /dev/null +++ b/sites/all/modules/examples/examples/phpcs.xml.dist @@ -0,0 +1,68 @@ + + + + Default PHP CodeSniffer configuration for Examples for Developers. + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sites/all/modules/examples/examples/phpunit_example/phpunit_example.info.yml b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.info.yml new file mode 100644 index 000000000..c99fe64d0 --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.info.yml @@ -0,0 +1,14 @@ +name: PHPUnit Example +type: module +description: Demontrates how to use PHPUnit-based tests. +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/phpunit_example/phpunit_example.links.menu.yml b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.links.menu.yml new file mode 100644 index 000000000..ec4b30eca --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.links.menu.yml @@ -0,0 +1,3 @@ +phpunit_example.description: + title: PHPUnit Example + route_name: phpunit_example_description diff --git a/sites/all/modules/examples/examples/phpunit_example/phpunit_example.module b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.module new file mode 100644 index 000000000..b02383de5 --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/phpunit_example.module @@ -0,0 +1,17 @@ +items[$item->getDisplayName()] = $item; + } + + /** + * A count of how many items exist. + * + * @return int + * The number of items that exist. + */ + public function countDisplayableItems() { + return count($this->items); + } + + /** + * All displayable items. + * + * @return array + * The displayable items. + */ + public function displayableItems() { + return $this->items; + } + + /** + * Find an item by its name. + * + * @param string $name + * The name to find. + * + * @return DisplayInfoInterface|null + * The found item, or NULL if none is found. + */ + public function item($name) { + if (isset($this->items[$name])) { + return $this->items[$name]; + } + return NULL; + } + +} diff --git a/sites/all/modules/examples/examples/phpunit_example/src/ProtectedPrivates.php b/sites/all/modules/examples/examples/phpunit_example/src/ProtectedPrivates.php new file mode 100644 index 000000000..0a2167d90 --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/src/ProtectedPrivates.php @@ -0,0 +1,58 @@ +add($a, $b); + } + + /** + * A simple addition method with validity checking. + * + * @param int|float $a + * A number to add. + * @param int|float $b + * Another number to add. + * + * @return numeric + * The sum of $a and $b. + * + * @throws \InvalidArgumentException + * If either $a or $b is non-numeric, we can't add, so we throw. + */ + private function privateAdd($a, $b) { + $adder = new AddClass(); + return $adder->add($a, $b); + } + +} diff --git a/sites/all/modules/examples/examples/phpunit_example/templates/description.html.twig b/sites/all/modules/examples/examples/phpunit_example/templates/description.html.twig new file mode 100644 index 000000000..249068771 --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/templates/description.html.twig @@ -0,0 +1,55 @@ +{# + +Description text for the PHPUnit Example. + +#} + +{% trans %} +

PHPUnit for Drupal: A very basic how-to.

+ +

How to use this example module

+

You really should be reading the various docblocks in the test files.

+ +

How To:

+
    +
  • PHPUnit tests belong in their own directory, so they won't be loaded + by the autoloader during normal bootstrap. This means you should have a + /tests/src directory in the root of your module directory.

    +
  • + +
  • Your tests should be in the Drupal\Tests\[your_module]\Unit + namespace. Under Drupal's PSR-4 system, this means your PHPUnit-based + tests should go in a [your_module]/tests/src/Unit + directory.

    +
  • +
  • Your test case should subclass Drupal\Tests\UnitTestCase. +

    +
  • +
  • +

    You can run PHPUnit-based tests from within Drupal 8 by enabling the + Testing module and then selecting the PHPUnit group from the testing page. + As of this writing, this method doesn't provide any useful output. +

    +
  • +
+ +

Standard PHPUnit Practices

+

You can (and really, should) run PHPUnit from the command line.

+

On unix-based systems this means you need to cd core and then +

./vendor/bin/phpunit
+

+

On Windows-based systems, assuming you have php in your path, +

php ./vendor/phpunit/phpunit/composer/bin/phpunit --group phpunit_example
+

+

Also, you should mark your tests as belonging to a group, so they can be run + independently. You do this by annotating your test classes with + @group group_name. You should have a @group for + your module name, and you should also have a @group for + integrations, such as views. +

+

So, for instance, to run all of the PHPUnit example tests, you would type +

./vendor/bin/phpunit --group phpunit_example
+

+

As you can see, including a @group annotation is a good idea.

+ +{% endtrans %} diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php new file mode 100644 index 000000000..75f9f2ce4 --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php @@ -0,0 +1,70 @@ + link relationships to check for. + * The key is the path to the page where our link should appear. + * The value is the link that should appear on that page. + */ + protected function providerMenuLinks() { + return [ + '' => '/examples/phpunit-example', + ]; + } + + /** + * Verify and validate that default menu links were loaded for this module. + */ + public function testPhpUnitExampleLink() { + $links = $this->providerMenuLinks(); + foreach ($links as $page => $path) { + $this->drupalGet($page); + $this->assertLinkByHref($path); + } + } + + /** + * Tests phpunit_example menus. + */ + public function testPhpUnitExampleMenu() { + $this->drupalGet('/examples/phpunit-example'); + $this->assertResponse(200, 'Description page exists.'); + } + +} diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/AddClassTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/AddClassTest.php new file mode 100644 index 000000000..4d5d0cae6 --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/AddClassTest.php @@ -0,0 +1,180 @@ +assertEquals($sut->add(2, 3), 5); + } + + /** + * Test AddClass::add() with a data provider method. + * + * This method is very similar to testAdd(), but uses a data provider method + * to test with a wider range of data. + * + * You can tell PHPUnit which method is the data provider using the + * '@dataProvider' annotation. + * + * The data provider method just returns a big array of arrays of arguments. + * That is, for each time you want this test method run, the data provider + * should create an array of arguments for this method. In this case, it's + * $expected, $a, and $b. So one set of arguments would look a bit like this + * pseudocode: + * + * @code + * array( valueForExpected, valueForA, valueForB ) + * @endcode + * + * It would then wrap this up in a higher-level array, so that PHPUnit can + * loop through them, like this pseudocode: + * + * @code + * return array( array(first, set), array (next, set) ); + * @endcode + * + * This test has a better methodology than testAdd(), because it can easily + * be adapted by other developers, and because it tries more than one data + * set. This test is much better than testAdd(), although it still only + * tests 'good' data. When combined with testAddWithBadDataProvider(), + * we get a better picture of the behavior of the method under test. + * + * @dataProvider addDataProvider + * + * @see AddClassTest::addDataProvider() + */ + public function testAddWithDataProvider($expected, $a, $b) { + $sut = new AddClass(); + $this->assertEquals($expected, $sut->add($a, $b)); + } + + /** + * Test AddClass::add() with data that should throw an exception. + * + * This method is similar to testAddWithDataProvider(), but the data + * provider gives us data that should throw an exception. + * + * This test uses the '@expectedException' annotation to tell PHPUnit that + * a thrown exception should pass the test. You specify a + * fully-qualified exception class name. If you specify \Exception, PHPUnit + * will pass any exception, whereas a more specific subclass of \Exception + * will require that exception type to be thrown. + * + * Alternately, you can use try and catch blocks with assertions in order + * to test exceptions. We won't demonstrate that here; it's a much better + * idea to test your exceptions with @expectedException. + * + * @dataProvider addBadDataProvider + * + * @expectedException \InvalidArgumentException + * + * @see AddClassTest::addBadDataProvider() + */ + public function testAddWithBadDataProvider($a, $b) { + $sut = new AddClass(); + $sut->add($a, $b); + } + + /** + * Data provider for testAddWithDataProvider(). + * + * Data provider methods take no arguments and return an array of data + * to use for tests. Each element of the array is another array, which + * corresponds to the arguments in the test method's signature. + * + * Note also that PHPUnit tries to run tests using methods that begin + * with 'test'. This means that data provider method names should not + * begin with 'test'. Also, by convention, they should end with + * 'DataProvider'. + * + * @return array + * Nested arrays of values to check: + * - $a + * - $b + * - $expected + * + * @see AddClassTest::testAddWithDataProvider() + */ + public function addDataProvider() { + return [ + [5, 2, 3], + [50, 20, 30], + ]; + } + + /** + * Data provider for testAddWithBadDataProvider(). + * + * Since AddClass::add() can throw exceptions, it's time + * to give it some data that will cause these exceptions. + * + * add() should throw exceptions if either of it's arguments are + * not numeric, and we will generate some test data to prove that + * this is what it actually does. + * + * @see AddClassTest::testAddWithBadDataProvider() + */ + public function addBadDataProvider() { + $bad_data = []; + // Set up an array with data that should cause add() + // to throw an exception. + $bad_data_types = ['string', FALSE, ['foo'], new \stdClass()]; + // Create some data where both $a and $b are bad types. + foreach ($bad_data_types as $bad_datum_a) { + foreach ($bad_data_types as $bad_datum_b) { + $bad_data[] = [$bad_datum_a, $bad_datum_b]; + } + } + // Create some data where $a is good and $b is bad. + foreach ($bad_data_types as $bad_datum_b) { + $bad_data[] = [1, $bad_datum_b]; + } + // Create some data where $b is good and $a is bad. + foreach ($bad_data_types as $bad_datum_a) { + $bad_data[] = [$bad_datum_a, 1]; + } + return $bad_data; + } + +} diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/DisplayManagerTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/DisplayManagerTest.php new file mode 100644 index 000000000..628ec4329 --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/DisplayManagerTest.php @@ -0,0 +1,59 @@ +getMock('Drupal\phpunit_example\DisplayInfoInterface'); + // Here we're illustrating that the mock object belongs to + // our interface. + $this->assertTrue($mock instanceof DisplayInfoInterface); + // 'Program' our mock object to return a value for getDisplayName(). + // expects($this->any()) tells the mock to return this value any time + // the method is called. + $mock->expects($this->any()) + ->method('getDisplayName') + ->will($this->returnValue('the display name')); + + // Create a DisplayManager, the class we're actually testing here. + $dm = new DisplayManager(); + // Give it the mocked info object. + $dm->addDisplayableItem($mock); + // Assert that our DisplayManager has exactly one display object (our mock). + $this->assertEquals(1, $dm->countDisplayableItems()); + // Assert that the DisplayManager can find our mocked info object. + $this->assertSame($mock, $dm->item('the display name')); + // Assert that the DisplayManager can't find an info object + // that it shouldn't have. + $this->assertNull($dm->item('nonexistant')); + } + +} diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/ProtectedPrivatesTest.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/ProtectedPrivatesTest.php new file mode 100644 index 000000000..e67bfbd9c --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/ProtectedPrivatesTest.php @@ -0,0 +1,149 @@ +getMethod($method_name); + $method->setAccessible(TRUE); + return $method; + } + + /** + * Good data provider. + */ + public function addDataProvider() { + return [ + [5, 2, 3], + ]; + } + + /** + * Test ProtectedPrivate::privateAdd(). + * + * We want to test a private method on a class. This is problematic + * because, by design, we don't have access to this method. However, + * we do have a tool available to help us out with this problem: + * We can override the accessibility of a method using reflection. + * + * @dataProvider addDataProvider + */ + public function testPrivateAdd($expected, $a, $b) { + // Get a reflected, accessible version of the privateAdd() method. + $private_method = $this->getAccessibleMethod( + 'Drupal\phpunit_example\ProtectedPrivates', + 'privateAdd' + ); + // Create a new ProtectedPrivates object. + $pp = new ProtectedPrivates(); + // Use the reflection to invoke on the object. + $sum = $private_method->invokeArgs($pp, [$a, $b]); + // Make an assertion. + $this->assertEquals($expected, $sum); + } + + /** + * Bad data provider. + */ + public function addBadDataProvider() { + return [ + ['string', []], + ]; + } + + /** + * Test ProtectedPrivate::privateAdd() with bad data. + * + * This is essentially the same test as testPrivateAdd(), but using + * non-numeric data. This lets us test the exception-throwing ability + * of this private method. + * + * @expectedException \InvalidArgumentException + * + * @dataProvider addBadDataProvider + */ + public function testPrivateAddBadData($a, $b) { + // Get a reflected, accessible version of the privateAdd() method. + $private_method = $this->getAccessibleMethod( + 'Drupal\phpunit_example\ProtectedPrivates', + 'privateAdd'); + // Create a new ProtectedPrivates object. + $pp = new ProtectedPrivates(); + // Use the reflection to invoke on the object. + // This should throw an exception. + $private_method->invokeArgs($pp, [$a, $b]); + } + + /** + * Test ProtectedPrivates::protectedAdd() using a stub class. + * + * We could use the same reflection technique to test protected + * methods, just like we did with private ones. + * + * But sometimes it might make more sense to use a stub class + * which will have access to the protected method. That's what + * we'll demonstrate here. + * + * @dataProvider addDataProvider + */ + public function testProtectedAdd($expected, $a, $b) { + $stub = new ProtectedPrivatesSubclass(); + $this->assertEquals($expected, $stub->subclassProtectedAdd($a, $b)); + } + + /** + * Test ProtectedPrivates::protectedAdd() with bad data using a stub class. + * + * This test is similar to testProtectedAdd(), but expects an exception. + * + * @expectedException \InvalidArgumentException + * + * @dataProvider addBadDataProvider + */ + public function testProtectedAddBadData($a, $b) { + $stub = new ProtectedPrivatesSubclass(); + $stub->subclassProtectedAdd($a, $b); + } + +} diff --git a/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/Subclasses/ProtectedPrivatesSubclass.php b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/Subclasses/ProtectedPrivatesSubclass.php new file mode 100644 index 000000000..a4b5bcb7e --- /dev/null +++ b/sites/all/modules/examples/examples/phpunit_example/tests/src/Unit/Subclasses/ProtectedPrivatesSubclass.php @@ -0,0 +1,30 @@ +protectedAdd($a, $b); + } + +} diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.api.php b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.api.php new file mode 100644 index 000000000..e3f6e3a3c --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.api.php @@ -0,0 +1,24 @@ + $plugin_definition) { + $sandwich_plugin_info[$plugin_id]['foobar'] = t('We have altered this in the alter hook'); + } +} diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.info.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.info.yml new file mode 100644 index 000000000..242fbff80 --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.info.yml @@ -0,0 +1,14 @@ +name: Plugin Type Example +type: module +description: Provides an example of defining a plugin type. +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.links.menu.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.links.menu.yml new file mode 100644 index 000000000..c607e3627 --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.links.menu.yml @@ -0,0 +1,3 @@ +plugin_type_example.description: + title: Plugin Type Example + route_name: plugin_type_example.description diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.module b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.module new file mode 100644 index 000000000..eea5ec8a9 --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.module @@ -0,0 +1,71 @@ + $plugin_definition) { + // Always uppercase the word rocket in sandwich descriptions. + $sandwich_plugin_info[$plugin_id]['description'] = str_replace(t('rocket'), t('ROCKET'), $sandwich_plugin_info[$plugin_id]['description']); + } +} diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.routing.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.routing.yml new file mode 100644 index 000000000..35d6acfde --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.routing.yml @@ -0,0 +1,8 @@ +# This defines our example page's path to the routing system. +plugin_type_example.description: + path: '/examples/plugin-type-example' + defaults: + _controller: '\Drupal\plugin_type_example\Controller\PluginTypeExampleController::description' + _title: 'Plugin Type Example' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.services.yml b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.services.yml new file mode 100644 index 000000000..db658ff56 --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/plugin_type_example.services.yml @@ -0,0 +1,11 @@ +# This declares the plugin manager to the service container. For background +# information on the service container, see https://www.drupal.org/node/2133171. +# Changes here require that the cache be cleared in order to have Drupal notice +# them. +services: + # The machine name of the service. This is the string that must be passed to + # Drupal::service() to get the instantiated plugin manager. + plugin.manager.sandwich: + # This tells the service container the name of our plugin manager class. + class: Drupal\plugin_type_example\SandwichPluginManager + parent: default_plugin_manager diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/Annotation/Sandwich.php b/sites/all/modules/examples/examples/plugin_type_example/src/Annotation/Sandwich.php new file mode 100644 index 000000000..29a7f65ce --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/src/Annotation/Sandwich.php @@ -0,0 +1,47 @@ +sandwichManager = $sandwich_manager; + } + + /** + * Displays a page with an overview of our plugin type and plugins. + * + * Lists all the Sandwich plugin definitions by using methods on the + * \Drupal\plugin_type_example\SandwichPluginManager class. Lists out the + * description for each plugin found by invoking methods defined on the + * plugins themselves. You can find the plugins we have defined in the + * \Drupal\plugin_type_example\Plugin\Sandwich namespace. + * + * @return array + * Render API array with content for the page at + * /examples/plugin_type_example. + */ + public function description() { + $build = []; + + $build['intro'] = [ + '#markup' => t("This page lists the sandwich plugins we've created. The sandwich plugin type is defined in Drupal\\plugin_type_example\\SandwichPluginManager. The various plugins are defined in the Drupal\\plugin_type_example\\Plugin\\Sandwich namespace."), + ]; + + // Get the list of all the sandwich plugins defined on the system from the + // plugin manager. Note that at this point, what we have is *definitions* of + // plugins, not the plugins themselves. + $sandwich_plugin_definitions = $this->sandwichManager->getDefinitions(); + + // Let's output a list of the plugin definitions we now have. + $items = []; + foreach ($sandwich_plugin_definitions as $sandwich_plugin_definition) { + // Here we use various properties from the plugin definition. These values + // are defined in the annotation at the top of the plugin class: see + // \Drupal\plugin_type_example\Plugin\Sandwich\ExampleHamSandwich. + $items[] = t("@id (calories: @calories, description: @description )", [ + '@id' => $sandwich_plugin_definition['id'], + '@calories' => $sandwich_plugin_definition['calories'], + '@description' => $sandwich_plugin_definition['description'], + ]); + } + + // Add our list to the render array. + $build['plugin_definitions'] = [ + '#theme' => 'item_list', + '#title' => 'Sandwich plugin definitions', + '#items' => $items, + ]; + + // If we want just a single plugin definition, we can use getDefinition(). + // This requires us to know the ID of the plugin we want. This is set in the + // annotation on the plugin class: see + // \Drupal\plugin_type_example\Plugin\Sandwich\ExampleHamSandwich. + $ham_sandwich_plugin_definition = $this->sandwichManager->getDefinition('meatball_sandwich'); + + // To get an instance of a plugin, we call createInstance() on the plugin + // manager, passing the ID of the plugin we want to load. Let's output a + // list of the plugins by loading an instance of each plugin definition and + // collecting the description from each. + $items = []; + // The array of plugin definitions is keyed by plugin id, so we can just use + // that to load our plugin instances. + foreach ($sandwich_plugin_definitions as $plugin_id => $sandwich_plugin_definition) { + // We now have a plugin instance. From here on it can be treated just as + // any other object; have its properties examined, methods called, etc. + $plugin = $this->sandwichManager->createInstance($plugin_id, ['of' => 'configuration values']); + $items[] = $plugin->description(); + } + + $build['plugins'] = [ + '#theme' => 'item_list', + '#title' => 'Sandwich plugins', + '#items' => $items, + ]; + + return $build; + } + + /** + * {@inheritdoc} + * + * Override the parent method so that we can inject our sandwich plugin + * manager service into the controller. + * + * For more about how dependency injection works read + * https://www.drupal.org/node/2133171 + * + * @see container + */ + public static function create(ContainerInterface $container) { + // Inject the plugin.manager.sandwich service that represents our plugin + // manager as defined in the plugin_type_example.services.yml file. + return new static($container->get('plugin.manager.sandwich')); + } + +} diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php b/sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php new file mode 100644 index 000000000..f9749cfc7 --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php @@ -0,0 +1,51 @@ +t() for string translations in our plugin. + use StringTranslationTrait; + + /** + * The day the sandwich is ordered. + * + * Since meatball sandwiches have a special behavior on Sundays, and since we + * want to test that behavior on days other than Sunday, we have to store the + * day as a property so we can test it. + * + * This is the string representation of the day of the week you get from + * date('D'). + * + * @var string + */ + protected $day; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + // This class needs to translate strings, so we need to inject the string + // translation service from the container. This means our plugin class has + // to implement ContainerFactoryPluginInterface. This requires that we make + // this create() method, and use it to inject services from the container. + // @see https://www.drupal.org/node/2012118 + $sandwich = new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('string_translation') + ); + return $sandwich; + } + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, TranslationInterface $translation) { + // Store the translation service. + $this->setStringTranslation($translation); + // Store the day so we can generate a special description on Sundays. + $this->day = date('D'); + // Pass the other parameters up to the parent constructor. + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function order(array $extras) { + $ingredients = ['meatballs', 'irresistible marinara sauce']; + $sandwich = array_merge($ingredients, $extras); + return 'You ordered an ' . implode(', ', $sandwich) . ' sandwich. Enjoy!'; + } + + /** + * {@inheritdoc} + */ + public function description() { + // We override the description() method in order to change the description + // text based on the date. On Sunday we only have day old bread. + if ($this->day == 'Sun') { + return $this->t("Italian style meatballs drenched in irresistible marinara sauce, served on day old bread."); + } + return parent::description(); + } + +} diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/SandwichBase.php b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichBase.php new file mode 100644 index 000000000..8a1cfc844 --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichBase.php @@ -0,0 +1,53 @@ +pluginDefinition['description']; + } + + /** + * {@inheritdoc} + */ + public function calories() { + // Retrieve the @calories property from the annotation and return it. + return (float) $this->pluginDefinition['calories']; + } + + /** + * {@inheritdoc} + */ + abstract public function order(array $extras); + +} diff --git a/sites/all/modules/examples/examples/plugin_type_example/src/SandwichInterface.php b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichInterface.php new file mode 100644 index 000000000..e8a5fe3ef --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/src/SandwichInterface.php @@ -0,0 +1,54 @@ +alterInfo('sandwich_info'); + + // This sets the caching method for our plugin definitions. Plugin + // definitions are discovered by examining the $subdir defined above, for + // any classes with an $plugin_definition_annotation_name. The annotations + // are read, and then the resulting data is cached using the provided cache + // backend. For our Sandwich plugin type, we've specified the @cache.default + // service be used in the plugin_type_example.services.yml file. The second + // argument is a cache key prefix. Out of the box Drupal with the default + // cache backend setup will store our plugin definition in the cache_default + // table using the sandwich_info key. All that is implementation details + // however, all we care about it that caching for our plugin definition is + // taken care of by this call. + $this->setCacheBackend($cache_backend, 'sandwich_info'); + } + +} diff --git a/sites/all/modules/examples/examples/plugin_type_example/tests/src/Functional/PluginTypeExampleTest.php b/sites/all/modules/examples/examples/plugin_type_example/tests/src/Functional/PluginTypeExampleTest.php new file mode 100644 index 000000000..81a600781 --- /dev/null +++ b/sites/all/modules/examples/examples/plugin_type_example/tests/src/Functional/PluginTypeExampleTest.php @@ -0,0 +1,82 @@ +container->get('plugin.manager.sandwich'); + + $sandwich_plugin_definitions = $manager->getDefinitions(); + + $this->assertCount(2, $sandwich_plugin_definitions, 'There are not two sandwich plugins defined.'); + + // Check some of the properties of the ham sandwich plugin definition. + $sandwich_plugin_definition = $sandwich_plugin_definitions['ham_sandwich']; + $this->assertEquals(426, $sandwich_plugin_definition['calories'], 'The ham sandwich plugin definition\'s calories property is not set.'); + + // Create an instance of the ham sandwich plugin to check it works. + $plugin = $manager->createInstance('ham_sandwich', ['of' => 'configuration values']); + + $this->assertInstanceOf(ExampleHamSandwich::class, $plugin); + + // Create a meatball sandwich so we can check it's special behavior on + // Sundays. + /* @var $meatball \Drupal\plugin_type_example\SandwichInterface */ + $meatball = $manager->createInstance('meatball_sandwich'); + // Set the $day property to 'Sun'. + $ref_day = new \ReflectionProperty($meatball, 'day'); + $ref_day->setAccessible(TRUE); + $ref_day->setValue($meatball, 'Sun'); + // Check the special description on Sunday. + $this->assertEqual($meatball->description(), 'Italian style meatballs drenched in irresistible marinara sauce, served on day old bread.'); + } + + /** + * Test the output of the example page. + */ + public function testPluginExamplePage() { + $assert = $this->assertSession(); + + $this->drupalGet('examples/plugin-type-example'); + $assert->statusCodeEquals(200); + + // Check we see the plugin id. + $assert->pageTextContains('ham_sandwich', 'The plugin ID was not output.'); + + // Check we see the plugin description. + $assert->pageTextContains('Ham, mustard, rocket, sun-dried tomatoes.', 'The plugin description was not output.'); + } + +} diff --git a/sites/all/modules/examples/examples/queue_example/queue_example.info.yml b/sites/all/modules/examples/examples/queue_example/queue_example.info.yml new file mode 100644 index 000000000..4a087ee1d --- /dev/null +++ b/sites/all/modules/examples/examples/queue_example/queue_example.info.yml @@ -0,0 +1,13 @@ +name: Queue Example +type: module +description: Examples of using the Drupal Queue API. +package: Example modules +# core: 8.x +dependencies: + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/queue_example/queue_example.links.menu.yml b/sites/all/modules/examples/examples/queue_example/queue_example.links.menu.yml new file mode 100644 index 000000000..29b86148c --- /dev/null +++ b/sites/all/modules/examples/examples/queue_example/queue_example.links.menu.yml @@ -0,0 +1,3 @@ +queue_example: + title: Queue Example + route_name: queue_example diff --git a/sites/all/modules/examples/examples/queue_example/queue_example.module b/sites/all/modules/examples/examples/queue_example/queue_example.module new file mode 100644 index 000000000..b3b0712b4 --- /dev/null +++ b/sites/all/modules/examples/examples/queue_example/queue_example.module @@ -0,0 +1,37 @@ +queueFactory = $queue_factory; + $this->queueType = $settings->get('queue_default', 'queue.database'); + $this->database = $database; + $this->cron = $cron; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('queue'), $container->get('database'), $container->get('cron'), $container->get('settings')); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + // Return a string that is the unique ID of our form. Best practice here is + // to namespace the form based on your module's name. + return 'queue_example'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Simple counter that makes it possible to put auto-incrementing default + // string into the string to insert. + if (empty($form_state->get('insert_counter'))) { + $form_state->set('insert_counter', 1); + } + + $queue_name = $form_state->getValue('queue_name') ?: 'queue_example_first_queue'; + $items = $this->retrieveQueue($queue_name); + + $form['help'] = [ + '#type' => 'markup', + '#markup' => '
' . $this->t('This page is an interface on the Drupal queue API. You can add new items to the queue, "claim" one (retrieve the next item and keep a lock on it), and delete one (remove it from the queue). Note that claims are not expired until cron runs, so there is a special button to run cron to perform any necessary expirations.') . '
', + ]; + + $form['wrong_queue_warning'] = [ + '#type' => 'markup', + '#markup' => '
' . $this->t('Note: the example works only with the default queue implementation, which is not currently configured!!') . '
', + '#access' => (!$this->doesQueueUseDB()), + ]; + + $queue_names = ['queue_example_first_queue', 'queue_example_second_queue']; + $form['queue_name'] = [ + '#type' => 'select', + '#title' => $this->t('Choose queue to examine'), + '#options' => array_combine($queue_names, $queue_names), + '#default_value' => $queue_name, + ]; + + $form['queue_show'] = [ + '#type' => 'submit', + '#value' => $this->t('Show queue'), + '#submit' => [[$this, 'submitShowQueue']], + ]; + + $form['status_fieldset'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Queue status for @name', ['@name' => $queue_name]), + '#collapsible' => TRUE, + ]; + + if (count($items) > 0) { + $form['status_fieldset']['status'] = [ + '#theme' => 'table', + '#header' => [ + $this->t('Item ID'), + $this->t('Claimed/Expiration'), + $this->t('Created'), + $this->t('Content/Data'), + ], + '#rows' => array_map([$this, 'processQueueItemForTable'], $items), + ]; + } + else { + $form['status_fieldset']['status'] = [ + '#type' => 'markup', + '#markup' => $this->t('There are no items in the queue.'), + ]; + } + + $form['insert_fieldset'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Insert into @name', ['@name' => $queue_name]), + ]; + + $form['insert_fieldset']['string_to_add'] = [ + '#type' => 'textfield', + '#size' => 10, + '#default_value' => $this->t('item @counter', ['@counter' => $form_state->get('insert_counter')]), + ]; + + $form['insert_fieldset']['add_item'] = [ + '#type' => 'submit', + '#value' => $this->t('Insert into queue'), + '#submit' => [[$this, 'submitAddQueueItem']], + ]; + + $form['claim_fieldset'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Claim from queue'), + '#collapsible' => TRUE, + ]; + + $form['claim_fieldset']['claim_time'] = [ + '#type' => 'radios', + '#title' => $this->t('Claim time, in seconds'), + '#options' => [ + 0 => $this->t('none'), + 5 => $this->t('5 seconds'), + 60 => $this->t('60 seconds'), + ], + '#description' => $this->t('This time is only valid if cron runs during this time period. You can run cron manually below.'), + '#default_value' => $form_state->getValue('claim_time') ?: 5, + ]; + + $form['claim_fieldset']['claim_item'] = [ + '#type' => 'submit', + '#value' => $this->t('Claim the next item from the queue'), + '#submit' => [[$this, 'submitClaimItem']], + ]; + + $form['claim_fieldset']['claim_and_delete_item'] = [ + '#type' => 'submit', + '#value' => $this->t('Claim the next item and delete it'), + '#submit' => [[$this, 'submitClaimDeleteItem']], + ]; + + $form['claim_fieldset']['run_cron'] = [ + '#type' => 'submit', + '#value' => $this->t('Run cron manually to expire claims'), + '#submit' => [[$this, 'submitRunCron']], + ]; + + $form['delete_queue'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete the queue and items in it'), + '#submit' => [[$this, 'submitDeleteQueue']], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + + /** + * Retrieves the queue from the database for display purposes only. + * + * It is not recommended to access the database directly, and this is only + * here so that the user interface can give a good idea of what's going on + * in the queue. + * + * @param string $queue_name + * The name of the queue from which to fetch items. + * + * @return array + * An array of item arrays. + */ + public function retrieveQueue($queue_name) { + $items = []; + + // This example requires the default queue implementation to work, + // so we bail if some other queue implementation has been installed. + if (!$this->doesQueueUseDb()) { + return $items; + } + + // Make sure there are queue items available. The queue will not create our + // database table if there are no items. + if ($this->queueFactory->get($queue_name)->numberOfItems() >= 1) { + $result = $this->database->query('SELECT item_id, data, expire, created FROM {' . DatabaseQueue::TABLE_NAME . '} WHERE name = :name ORDER BY item_id', + [':name' => $queue_name], + ['fetch' => \PDO::FETCH_ASSOC] + ); + foreach ($result as $item) { + $items[] = $item; + } + } + + return $items; + } + + /** + * Check if we are using the default database queue. + * + * @return bool + * TRUE if we are using the default database queue implementation. + */ + protected function doesQueueUseDb() { + return $this->queueType == 'queue.database'; + } + + /** + * Submit function for the show-queue button. + * + * @param array $form + * Form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + */ + public function submitShowQueue(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + + // Get the number of items. + $count = $queue->numberOfItems(); + + // Update the form item counter. + $form_state->set('insert_counter', $count + 1); + + // Unset the string_to_add textbox. + $form_state->unsetValue('string_to_add'); + + $form_state->setRebuild(); + } + + /** + * Submit function for the insert-into-queue button. + * + * @param array $form + * Form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + */ + public function submitAddQueueItem(array &$form, FormStateInterface $form_state) { + // Get a queue (of the default type) called 'queue_example_queue'. + // If the default queue class is SystemQueue this creates a queue that + // stores its items in the database. + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + + // Queue the string. + $queue->createItem($form_state->getValue('string_to_add')); + $count = $queue->numberOfItems(); + drupal_set_message($this->t('Queued your string (@string_to_add). There are now @count items in the queue.', ['@count' => $count, '@string_to_add' => $form_state->getValue('string_to_add')])); + // Allows us to keep information in $form_state. + $form_state->setRebuild(); + + // Unsetting the string_to_add allows us to set the incremented default + // value for the user so they don't have to type anything. + $form_state->unsetValue('string_to_add'); + $form_state->set('insert_counter', $count + 1); + } + + /** + * Submit function for the "claim" button. + * + * Claims (retrieves) an item from the queue and reports the results. + * + * @param array $form + * Form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + */ + public function submitClaimItem(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + $item = $queue->claimItem($form_state->getValue('claim_time')); + $count = $queue->numberOfItems(); + if (!empty($item)) { + drupal_set_message($this->t('Claimed item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.', + [ + '@count' => $count, + '@item_id' => $item->item_id, + '@string' => $item->data, + '@seconds' => $form_state->getValue('claim_time'), + ])); + } + else { + drupal_set_message($this->t('There were no items in the queue available to claim. There are @count items in the queue.', ['@count' => $count])); + } + $form_state->setRebuild(); + } + + /** + * Submit function for "Claim and delete" button. + * + * @param array $form + * Form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + */ + public function submitClaimDeleteItem(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // There is no harm in trying to recreate existing. + $queue->createQueue(); + $count = $queue->numberOfItems(); + $item = $queue->claimItem(60); + if (!empty($item)) { + drupal_set_message($this->t('Claimed and deleted item id=@item_id string=@string for @seconds seconds. There are @count items in the queue.', [ + '@count' => $count, + '@item_id' => $item->item_id, + '@string' => $item->data, + '@seconds' => $form_state->getValue('claim_time'), + ])); + $queue->deleteItem($item); + $count = $queue->numberOfItems(); + drupal_set_message($this->t('There are now @count items in the queue.', ['@count' => $count])); + } + else { + $count = $queue->numberOfItems(); + drupal_set_message($this->t('There were no items in the queue available to claim/delete. There are currently @count items in the queue.', ['@count' => $count])); + } + $form_state->setRebuild(); + } + + /** + * Submit function for "run cron" button. + * + * Runs cron (to release expired claims) and reports the results. + * + * @param array $form + * Form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + */ + public function submitRunCron(array &$form, FormStateInterface $form_state) { + $this->cron->run(); + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + // @see https://www.drupal.org/node/2705809 + if ($queue instanceof QueueGarbageCollectionInterface) { + $queue->garbageCollection(); + } + // There is no harm in trying to recreate existing. + $queue->createQueue(); + $count = $queue->numberOfItems(); + drupal_set_message($this->t('Ran cron. If claimed items expired, they should be expired now. There are now @count items in the queue', ['@count' => $count])); + $form_state->setRebuild(); + } + + /** + * Submit handler for clearing/deleting the queue. + * + * @param array $form + * Form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + */ + public function submitDeleteQueue(array &$form, FormStateInterface $form_state) { + $queue = $this->queueFactory->get($form_state->getValue('queue_name')); + $queue->deleteQueue(); + drupal_set_message($this->t('Deleted the @queue_name queue and all items in it', ['@queue_name' => $form_state->getValue('queue_name')])); + } + + /** + * Helper method to format a queue item for display in a summary table. + * + * @param array $item + * Queue item array with keys for item_id, expire, created, and data. + * + * @return array + * An array with the queue properties in the right order for display in a + * summary table. + */ + private function processQueueItemForTable(array $item) { + if ($item['expire'] > 0) { + $item['expire'] = $this->t('Claimed: expires %expire', ['%expire' => date('r', $item['expire'])]); + } + else { + $item['expire'] = $this->t('Unclaimed'); + } + $item['created'] = date('r', $item['created']); + $item['content'] = Html::escape(unserialize($item['data'])); + unset($item['data']); + + return $item; + } + +} diff --git a/sites/all/modules/examples/examples/queue_example/tests/src/Functional/QueueExampleTest.php b/sites/all/modules/examples/examples/queue_example/tests/src/Functional/QueueExampleTest.php new file mode 100644 index 000000000..c7d809830 --- /dev/null +++ b/sites/all/modules/examples/examples/queue_example/tests/src/Functional/QueueExampleTest.php @@ -0,0 +1,68 @@ +drupalGet('examples/queue_example'); + // Load the queue with 5 items. + for ($i = 1; $i <= 5; $i++) { + $edit = ['queue_name' => 'queue_example_first_queue', 'string_to_add' => 'boogie' . $i]; + $this->drupalPostForm(NULL, $edit, 'Insert into queue'); + $this->assertText(t('There are now @number items in the queue', ['@number' => $i])); + } + // Claim each of the 5 items with a claim time of 0 seconds. + for ($i = 1; $i <= 5; $i++) { + $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0]; + $this->drupalPostForm(NULL, $edit, 'Claim the next item from the queue'); + $this->assertPattern(t('%Claimed item id=.*string=@string for 0 seconds.%', ['@string' => 'boogie' . $i])); + } + $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0]; + $this->drupalPostForm(NULL, $edit, 'Claim the next item from the queue'); + $this->assertText('There were no items in the queue available to claim'); + + // Sleep a second so we can make sure that the timeouts actually time out. + // Local systems work fine with this but apparently the PIFR server is so + // fast that it needs a sleep before the cron run. + sleep(1); + + // Run cron to release expired items. + $this->drupalPostForm(NULL, [], 'Run cron manually to expire claims'); + + // Claim and delete each of the 5 items which should now be available. + for ($i = 1; $i <= 5; $i++) { + $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0]; + $this->drupalPostForm(NULL, $edit, 'Claim the next item and delete it'); + $this->assertPattern(t('%Claimed and deleted item id=.*string=@string for 0 seconds.%', ['@string' => 'boogie' . $i])); + } + // Verify that nothing is left to claim. + $edit = ['queue_name' => 'queue_example_first_queue', 'claim_time' => 0]; + $this->drupalPostForm(NULL, $edit, 'Claim the next item from the queue'); + $this->assertText('There were no items in the queue available to claim'); + } + +} diff --git a/sites/all/modules/examples/examples/simpletest_example/config/install/core.entity_form_display.node.simpletest_example.default.yml b/sites/all/modules/examples/examples/simpletest_example/config/install/core.entity_form_display.node.simpletest_example.default.yml new file mode 100644 index 000000000..959c8c93b --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/config/install/core.entity_form_display.node.simpletest_example.default.yml @@ -0,0 +1,64 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.simpletest_example.body + - node.type.simpletest_example + module: + - path + - text + enforced: + module: + - simpletest_example +id: node.simpletest_example.default +targetEntityType: node +bundle: simpletest_example +mode: default +content: + title: + type: string_textfield + weight: -5 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + third_party_settings: { } + created: + type: datetime_timestamp + weight: 10 + settings: { } + third_party_settings: { } + promote: + type: boolean_checkbox + weight: 15 + settings: + display_label: true + third_party_settings: { } + sticky: + type: boolean_checkbox + weight: 16 + settings: + display_label: true + third_party_settings: { } + path: + type: path + weight: 30 + settings: { } + third_party_settings: { } + body: + type: text_textarea_with_summary + weight: 31 + settings: + rows: 9 + summary_rows: 3 + placeholder: '' + third_party_settings: { } +hidden: { } +third_party_settings: { } diff --git a/sites/all/modules/examples/examples/simpletest_example/config/install/field.field.node.simpletest_example.body.yml b/sites/all/modules/examples/examples/simpletest_example/config/install/field.field.node.simpletest_example.body.yml new file mode 100644 index 000000000..6b5df1a90 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/config/install/field.field.node.simpletest_example.body.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.body + - node.type.simpletest_example + module: + - text + enforced: + module: + - simpletest_example +id: node.simpletest_example.body +field_name: body +entity_type: node +bundle: simpletest_example +label: Body +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + display_summary: false +third_party_settings: { } +field_type: text_with_summary diff --git a/sites/all/modules/examples/examples/simpletest_example/config/install/node.type.simpletest_example.yml b/sites/all/modules/examples/examples/simpletest_example/config/install/node.type.simpletest_example.yml new file mode 100644 index 000000000..06af9fcb6 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/config/install/node.type.simpletest_example.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - simpletest_example +name: 'SimpleTest Example Node Type' +type: simpletest_example +description: 'A content type that exists so we can test it.' +help: '' +new_revision: false +preview_mode: 1 +display_submitted: true +third_party_settings: { } diff --git a/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.info.yml b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.info.yml new file mode 100644 index 000000000..75add8f0f --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.info.yml @@ -0,0 +1,14 @@ +name: "SimpleTest Example Mock Module" +type: module +hidden: true +description: "Mock module for the SimpleTest Example module." +package: Example modules +# core: 8.x +dependencies: + - simpletest_example + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.module b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.module new file mode 100644 index 000000000..79974f615 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/mock/simpletest_example_test/simpletest_example_test.module @@ -0,0 +1,38 @@ +getType() == 'simpletest_example') { + $build['simpletest_example_test_section'] = [ + '#markup' => t('The test module did its thing.'), + '#weight' => -99, + ]; + } +} diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.info.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.info.yml new file mode 100644 index 000000000..8965094f6 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.info.yml @@ -0,0 +1,22 @@ +name: SimpleTest Example +type: module +hidden: false +description: 'Demonstrates some SimpleTest-based tests in Drupal 8.' +package: Example modules +# core: 8.x +# We have to be as explicit as possible about the dependencies for this module. +# If you look at the config info in the config/install/ directory, you'll see +# that they depend on some of these modules. +dependencies: + - drupal:simpletest + - drupal:node + - drupal:field + - drupal:path + - drupal:text + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.links.menu.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.links.menu.yml new file mode 100644 index 000000000..639a8cbc6 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.links.menu.yml @@ -0,0 +1,3 @@ +simpletest_example.description: + title: SimpleTest Example + route_name: simpletest_example_description diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.module b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.module new file mode 100644 index 000000000..62548bf8e --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.module @@ -0,0 +1,60 @@ +getType(); + // If it's not a simpletest_example node, or if it's not operations we care + // about, then just ignore. + if ($type != 'simpletest_example' || ($op != 'update' && $op != 'delete')) { + return AccessResult::neutral(); + } + + // This code has a BUG that we'll find in testing. + // + // This is the incorrect version we'll use to demonstrate test failure. + // The correct version should have ($op == 'update' || $op == 'delete'). + // The author had mistakenly always tested with User 1 so it always + // allowed access and the bug wasn't noticed! + if (($op == 'delete') && ($account->hasPermission('extra special edit any simpletest_example') && ($account->id() == $node->getAuthorId()))) { + return AccessResult::allowed(); + } + + return AccessResult::forbidden(); +} + +/** + * @} End of "defgroup simpletest_example". + */ diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.permissions.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.permissions.yml new file mode 100644 index 000000000..b250a73e3 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.permissions.yml @@ -0,0 +1,6 @@ +# In this case we're adding an addition permission that does the same +# as the one the node module offers, just to demonstrate this error. +'extra special edit any simpletest_example': + title: Extra special edit any SimpleTest Example + description: Allow user to edit any SimpleTest Example content authored by any user. + 'restrict access': TRUE diff --git a/sites/all/modules/examples/examples/simpletest_example/simpletest_example.routing.yml b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.routing.yml new file mode 100644 index 000000000..2bf0834ab --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/simpletest_example.routing.yml @@ -0,0 +1,8 @@ +# This module only has one route. +# It is to a page explaining the module. +simpletest_example_description: + path: 'examples/simpletest-example' + defaults: + _controller: '\Drupal\simpletest_example\Controller\SimpleTestExampleController::description' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/simpletest_example/src/Controller/SimpleTestExampleController.php b/sites/all/modules/examples/examples/simpletest_example/src/Controller/SimpleTestExampleController.php new file mode 100644 index 000000000..1277e1f89 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/src/Controller/SimpleTestExampleController.php @@ -0,0 +1,21 @@ +drupalCreateUser(['access content']); + // Log them in. + $this->drupalLogin($test_user); + // Set up some content. + $settings = [ + 'type' => 'simpletest_example', + 'title' => $this->randomMachineName(32), + ]; + // Create the content node. + $node = $this->drupalCreateNode($settings); + // View the node. + $this->drupalGet('node/' . $node->id()); + // Check that our module did it's thing. + $this->assertText(t('The test module did its thing.'), "Found evidence of test module."); + } + +} diff --git a/sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleTest.php b/sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleTest.php new file mode 100644 index 000000000..abf8cd0e4 --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/src/Tests/SimpleTestExampleTest.php @@ -0,0 +1,168 @@ +drupalGet(''); + $this->assertResponse(200, 'The Home page is available.'); + $this->assertLinkByHref('examples/simpletest-example'); + + // Verify that anonymous can access the simpletest_examples page. + $this->drupalGet('examples/simpletest-example'); + $this->assertResponse(200, 'The SimpleTest Example description page is available.'); + } + + /** + * Test node creation through the user interface. + * + * Creates a node using the node/add form and verifies its consistency in + * the database. + */ + public function testSimpleTestExampleCreate() { + // Create a user with the ability to create our content type. This + // permission is generated by the node module. + $user = $this->createUser(['create simpletest_example content']); + // Log in our user. + $this->drupalLogin($user); + + // Create a node using the node/add form. + $edit = []; + $edit['title[0][value]'] = $this->randomMachineName(8); + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->drupalPostForm('node/add/simpletest_example', $edit, 'Save'); + + // Check that our simpletest_example node has been created. + $this->assertText(t('@post @title has been created.', [ + '@post' => 'SimpleTest Example Node Type', + '@title' => $edit['title[0][value]'], + ])); + // Check that the node exists in the database. + $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); + $this->assertTrue($node, 'Node found in database.'); + + // Verify 'submitted by' information. Drupal adds a newline in there, so + // we have to check for that. + $username = $this->loggedInUser->getUsername(); + $datetime = format_date($node->getCreatedTime()); + $submitted_by = "Submitted by $username\n on $datetime"; + + $this->drupalGet('node/' . $node->id()); + $this->assertText($submitted_by); + } + + /** + * Create a simpletest_example node and then see if our user can edit it. + * + * Note that some assertions in this test will fail. We do this to show what + * a failing test looks like. Since we don't want this to interfere with + * automated tests, however, we jump through some hoops to determine our + * environment. + */ + public function testSimpleTestExampleEdit() { + // Create a user with our special permission. + $user = $this->drupalCreateUser(['extra special edit any simpletest_example']); + // Log in our user. + $this->drupalLogin($user); + + // Create a node with our user as the creator. + // drupalCreateNode() uses the logged-in user by default. + $settings = [ + 'type' => 'simpletest_example', + 'title' => $this->randomMachineName(32), + ]; + $node = $this->drupalCreateNode($settings); + + // For debugging, we might output some information using $this->verbose() + // It will only be output if the testing settings have 'verbose' set. + $this->verbose('Node created: ' . $node->getTitle()); + + // This section demonstrates a failing test. However, we want this test to + // pass when it's running on the Drupal QA testbot. So we need to determine + // which environment we're running inside of before we continue. + if (!$this->runningOnTestbot()) { + $this->drupalGet('node/' . $node->id() . '/edit'); + // The debug() statement will output information into the test results. + // It can also be used in Drupal anywhere in code and will come out + // as a drupal_set_message(). + debug('The following test should fail. Examine the verbose message above it to see why.'); + // Make sure we don't get a 401 unauthorized response: + $this->assertResponse(200, 'User is allowed to edit the content.'); + + // Looking for title text in the page to determine whether we were + // successful opening edit form. + $this->assertText(t("@title", ['@title' => $settings['title']]), "Found title in edit form"); + } + } + + /** + * Detect if we're running on PIFR testbot. + * + * We can skip intentional failure if we're on the testbot. It happens that + * on the testbot the site under test is in a directory named 'checkout' or + * 'site_under_test'. + * + * @return bool + * TRUE if running on testbot. + */ + public function runningOnTestbot() { + // @todo: Add this line back once the testbot variable is available. + // https://www.drupal.org/node/2565181 + // return env('DRUPALCI'); + return TRUE; + } + +} diff --git a/sites/all/modules/examples/examples/simpletest_example/templates/description.html.twig b/sites/all/modules/examples/examples/simpletest_example/templates/description.html.twig new file mode 100644 index 000000000..e47240d4e --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/templates/description.html.twig @@ -0,0 +1,18 @@ +{# + +Description text for the Simpletest Example. + +#} + +{% trans %} +

Please note that the use of SimpleTest is deprecated. This example module will +be removed in Drupal 9, and new tests should not be written using SimpleTest. In +addition, all existing SimpleTest tests should be converted to PHPUnit +functional tests.

+ +

+There are some instructions for how to convert Simpletest-based tests to the new +BrowserTestBase in this change notice: +https://www.drupal.org/node/2469723. +

+{% endtrans %} diff --git a/sites/all/modules/examples/examples/simpletest_example/tests/src/Functional/SimpletestExampeTest.php b/sites/all/modules/examples/examples/simpletest_example/tests/src/Functional/SimpletestExampeTest.php new file mode 100644 index 000000000..edff10d9c --- /dev/null +++ b/sites/all/modules/examples/examples/simpletest_example/tests/src/Functional/SimpletestExampeTest.php @@ -0,0 +1,48 @@ +assertSession(); + + // The simpletest_example module should have been installed by the test, so + // we can just uninstall it. + /* @var $module_installer \Drupal\Core\Extension\ModuleInstallerInterface */ + $module_installer = $this->container->get('module_installer'); + $module_installer->uninstall(['simpletest_example']); + $this->drupalGet('examples/simpletest-example'); + $session->statusCodeEquals(404); + + // We reinstall the simpletest_example module to make sure it happens + // properly. + $module_installer->install(['simpletest_example']); + $this->drupalGet('examples/simpletest-example'); + $session->statusCodeEquals(200); + } + +} diff --git a/sites/all/modules/examples/examples/src/Tests/ExamplesTest.php b/sites/all/modules/examples/examples/src/Tests/ExamplesTest.php new file mode 100644 index 000000000..e55717a76 --- /dev/null +++ b/sites/all/modules/examples/examples/src/Tests/ExamplesTest.php @@ -0,0 +1,73 @@ +drupalCreateUser(['access toolbar']); + $this->drupalLogin($user); + + // Check for the 'Examples' tab. + $this->drupalGet(''); + + // Assert that the toolbar tab registered by examples is present. + $this->assertLink('Examples'); + + // Assert that the toolbar tab registered by examples is present. + $this->assertEqual( + \count($this->xpath('//nav/div/a[@data-toolbar-tray="toolbar-item-examples-tray"]')), + 1, + 'Found the Examples toolbar tab.' + ); + + // Assert that the toolbar tray registered by examples is present. + $this->assertEqual( + \count($this->xpath('//nav/div/div[@data-toolbar-tray="toolbar-item-examples-tray"]')), + 1, + 'Found the Examples toolbar tray.' + ); + // Assert that PHPUnit link does not appears in the tray. + $phpunit_link = 'PHPUnit Example'; + $this->assertNoLink($phpunit_link); + $this->assertNoRaw('
  • '); + + // Install phpunit_example and see if it appears in the toolbar. We use + // phpunit_example because it's very light-weight. + $this->container->get('module_installer')->install(['phpunit_example'], TRUE); + // SimpleTest needs for us to reset all the caches. + $this->resetAll(); + + // Verify that PHPUnit appears in the tray. + $this->drupalGet(''); + $this->assertLink($phpunit_link); + // Assert that the PHPUnit tray item is present. + $this->assertEqual( + \count($this->xpath('//nav/div/div/nav/ul/li[@class="phpunit-example"]')), + 1, + 'Found the PHPUnit Example tray item.' + ); + + } + +} diff --git a/sites/all/modules/examples/examples/src/Tests/ExamplesTestBase.php b/sites/all/modules/examples/examples/src/Tests/ExamplesTestBase.php new file mode 100644 index 000000000..e920b5910 --- /dev/null +++ b/sites/all/modules/examples/examples/src/Tests/ExamplesTestBase.php @@ -0,0 +1,41 @@ +setupExamplesMenus(); + } + + /** + * Set up menus and tasks in their regions. + * + * Since menus and tasks are now blocks, we're required to explicitly set them + * to regions. This method standardizes the way we do that for Examples. + * + * Note that subclasses must explicitly declare that the block module is a + * dependency. + */ + protected function setupExamplesMenus() { + $this->drupalPlaceBlock('system_menu_block:tools', ['region' => 'primary_menu']); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'secondary_menu']); + $this->drupalPlaceBlock('local_actions_block', ['region' => 'content']); + $this->drupalPlaceBlock('page_title_block', ['region' => 'content']); + } + +} diff --git a/sites/all/modules/examples/examples/src/Utility/DescriptionTemplateTrait.php b/sites/all/modules/examples/examples/src/Utility/DescriptionTemplateTrait.php new file mode 100644 index 000000000..3498b25cb --- /dev/null +++ b/sites/all/modules/examples/examples/src/Utility/DescriptionTemplateTrait.php @@ -0,0 +1,78 @@ +getDescriptionTemplatePath(); + $template = file_get_contents($template_path); + $build = [ + 'description' => [ + '#type' => 'inline_template', + '#template' => $template, + '#context' => $this->getDescriptionVariables(), + ], + ]; + return $build; + } + + /** + * Name of our module. + * + * @return string + * A module name. + */ + abstract protected function getModuleName(); + + /** + * Variables to act as context to the twig template file. + * + * @return array + * Associative array that defines context for a template. + */ + protected function getDescriptionVariables() { + $variables = [ + 'module' => $this->getModuleName(), + ]; + return $variables; + } + + /** + * Get full path to the template. + * + * @return string + * Path string. + */ + protected function getDescriptionTemplatePath() { + return drupal_get_path('module', $this->getModuleName()) . "/templates/description.html.twig"; + } + +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/Controller/StreamWrapperExampleController.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/Controller/StreamWrapperExampleController.php new file mode 100644 index 000000000..7d607a054 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/Controller/StreamWrapperExampleController.php @@ -0,0 +1,24 @@ + [ + '#theme' => 'example_description', + ], + ]; + return $build; + } + +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php new file mode 100644 index 000000000..dd5836a33 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/PathProcessor/PathProcessorSessions.php @@ -0,0 +1,30 @@ +query->has('file')) { + $file_path = preg_replace('|^\/examples\/stream_wrapper_example\/files\/|', '', $path); + $request->query->set('file', $file_path); + // We return the route we want to match. + return '/examples/stream_wrapper_example/files'; + } + return $path; + } + +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php new file mode 100644 index 000000000..eaed8aa29 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/FileExampleSessionStreamWrapper.php @@ -0,0 +1,879 @@ +requestStack = \Drupal::service('request_stack'); + $helper = $this->getSessionWrapper(); + $helper->setPath('.isadir.txt', TRUE); + $this->streamMode = FALSE; + } + + /** + * Get wrapped session manipulators. + */ + public function getSessionWrapper() { + return new SessionWrapper($this->requestStack); + } + + /** + * Returns the name of the stream wrapper for use in the UI. + * + * @return string + * The stream wrapper name. + */ + public function getName() { + return t('File Example Session files'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return t('Simulated file system using your session storage. Not for real use!'); + } + + /** + * Implements setUri(). + */ + public function setUri($uri) { + $this->uri = $uri; + } + + /** + * Implements getUri(). + */ + public function getUri() { + return $this->uri; + } + + /** + * Implements getTarget(). + * + * The "target" is the portion of the URI to the right of the scheme. + * So in session://example/test.txt, the target is 'example/test.txt'. + * + * @todo Figure out what this is in the new API. + */ + public function getTarget($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + + list($scheme, $target) = explode('://', $uri, 2); + + // Remove erroneous leading or trailing, forward-slashes and backslashes. + // In the session:// scheme, there is never a leading slash on the target. + return trim($target, '\/'); + } + + /** + * Implements getDirectoryPath(). + * + * In this case there is no directory string, so return an empty string. + */ + public function getDirectoryPath() { + return ''; + } + + /** + * Overrides getExternalUrl(). + * + * We have set up a helper function and menu entry to provide access to this + * key via HTTP; normally it would be accessible some other way. + */ + public function getExternalUrl() { + $path = str_replace('\\', '/', $this->getTarget()); + return $this->url('stream_wrapper_example.files.session', ['filepath' => $path, 'scheme' => 'session'], ['absolute' => TRUE]); + } + + /** + * Returns canonical, absolute path of the resource. + * + * Implementation placeholder. PHP's realpath() does not support stream + * wrappers. We provide this as a default so that individual wrappers may + * implement their own solutions. + * + * @return string + * Returns a string with absolute pathname on success (implemented + * by core wrappers), or FALSE on failure or if the registered + * wrapper does not provide an implementation. + */ + public function realpath() { + return 'session://' . $this->getLocalPath(); + } + + /** + * Returns the local path. + * + * Here we aren't doing anything but stashing the "file" in a key in the + * $_SESSION variable, so there's not much to do but to create a "path" + * which is really just a key in the $_SESSION variable. So something + * like 'session://one/two/three.txt' becomes + * $_SESSION['stream_wrapper_example']['one']['two']['three.txt'] and the + * actual path is "one/two/three.txt". + * + * @param string $uri + * Optional URI, supplied when doing a move or rename. + */ + protected function getLocalPath($uri = NULL) { + if (!isset($uri)) { + $uri = $this->uri; + } + + $path = str_replace('session://', '', $uri); + $path = trim($path, '/'); + return $path; + } + + /** + * Opens a stream, as for fopen(), file_get_contents(), file_put_contents(). + * + * @param string $uri + * A string containing the URI to the file to open. + * @param string $mode + * The file mode ("r", "wb" etc.). + * @param int $options + * A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS. + * @param string &$opened_path + * A string containing the path actually opened. + * + * @return bool + * Returns TRUE if file was opened successfully. (Always returns TRUE). + * + * @see http://php.net/manual/en/streamwrapper.stream-open.php + */ + public function stream_open($uri, $mode, $options, &$opened_path) { + $this->uri = $uri; + $path = $this->getLocalPath($uri); + // We will support two modes only, 'r' and 'w'. If the key is 'r', + // check to make sure the file is there. + if (stristr($mode, 'r') !== FALSE) { + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path)) { + return FALSE; + } + else { + $buffer = $helper->getPath($path); + if (!is_string($buffer)) { + return FALSE; + } + $this->sessionContent = $buffer; + } + $this->streamMode = 'r'; + } + else { + $this->sessionContent = ''; + $this->streamMode = 'w'; + } + // Reset the stream pointer since this is an open. + $this->streamPointer = 0; + return TRUE; + } + + /** + * Retrieve the underlying stream resource. + * + * This method is called in response to stream_select(). + * + * @param int $cast_as + * Can be STREAM_CAST_FOR_SELECT when stream_select() is calling + * stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for + * other uses. + * + * @return resource|false + * The underlying stream resource or FALSE if stream_select() is not + * supported. + * + * @see stream_select() + * @see http://php.net/manual/streamwrapper.stream-cast.php + */ + public function stream_cast($cast_as) { + return FALSE; + } + + /** + * Sets metadata on the stream. + * + * @param string $path + * A string containing the URI to the file to set metadata on. + * @param int $option + * One of: + * - STREAM_META_TOUCH: The method was called in response to touch(). + * - STREAM_META_OWNER_NAME: The method was called in response to chown() + * with string parameter. + * - STREAM_META_OWNER: The method was called in response to chown(). + * - STREAM_META_GROUP_NAME: The method was called in response to chgrp(). + * - STREAM_META_GROUP: The method was called in response to chgrp(). + * - STREAM_META_ACCESS: The method was called in response to chmod(). + * @param mixed $value + * If option is: + * - STREAM_META_TOUCH: Array consisting of two arguments of the touch() + * function. + * - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner + * user/group as string. + * - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner + * user/group as integer. + * - STREAM_META_ACCESS: The argument of the chmod() as integer. + * + * @return bool + * Returns TRUE on success or FALSE on failure. If $option is not + * implemented, FALSE should be returned. + * + * @see http://www.php.net/manual/streamwrapper.stream-metadata.php + */ + public function stream_metadata($path, $option, $value) { + // We don't really do any of these, but we want to reassure the calling code + // that there is no problem with chown or chgrp, even though we do not + // actually support these. + return TRUE; + } + + /** + * Change stream options. + * + * This method is called to set options on the stream. + * + * @param int $option + * One of: + * - STREAM_OPTION_BLOCKING: The method was called in response to + * stream_set_blocking(). + * - STREAM_OPTION_READ_TIMEOUT: The method was called in response to + * stream_set_timeout(). + * - STREAM_OPTION_WRITE_BUFFER: The method was called in response to + * stream_set_write_buffer(). + * @param int $arg1 + * If option is: + * - STREAM_OPTION_BLOCKING: The requested blocking mode: + * - 1 means blocking. + * - 0 means not blocking. + * - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds. + * - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or + * STREAM_BUFFER_FULL. + * @param int $arg2 + * If option is: + * - STREAM_OPTION_BLOCKING: This option is not set. + * - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds. + * - STREAM_OPTION_WRITE_BUFFER: The requested buffer size. + * + * @return bool + * TRUE on success, FALSE otherwise. If $option is not implemented, FALSE + * should be returned. + */ + public function stream_set_option($option, $arg1, $arg2) { + return FALSE; + } + + /** + * Truncate stream. + * + * Will respond to truncation; e.g., through ftruncate(). + * + * @param int $new_size + * The new size. + * + * @return bool + * TRUE on success, FALSE otherwise. + * + * @todo + * This one actually makes sense for the example. + */ + public function stream_truncate($new_size) { + return FALSE; + } + + /** + * Support for flock(). + * + * The $_SESSION variable has no locking capability, so return TRUE. + * + * @param int $operation + * One of the following: + * - LOCK_SH to acquire a shared lock (reader). + * - LOCK_EX to acquire an exclusive lock (writer). + * - LOCK_UN to release a lock (shared or exclusive). + * - LOCK_NB if you don't want flock() to block while locking (not + * supported on Windows). + * + * @return bool + * Always returns TRUE at the present time. (no support) + * + * @see http://php.net/manual/en/streamwrapper.stream-lock.php + */ + public function stream_lock($operation) { + return TRUE; + } + + /** + * Support for fread(), file_get_contents() etc. + * + * @param int $count + * Maximum number of bytes to be read. + * + * @return string + * The string that was read, or FALSE in case of an error. + * + * @see http://php.net/manual/en/streamwrapper.stream-read.php + */ + public function stream_read($count) { + if (is_string($this->sessionContent)) { + $remaining_chars = strlen($this->sessionContent) - $this->streamPointer; + $number_to_read = min($count, $remaining_chars); + if ($remaining_chars > 0) { + $buffer = substr($this->sessionContent, $this->streamPointer, $number_to_read); + $this->streamPointer += $number_to_read; + return $buffer; + } + } + return FALSE; + } + + /** + * Support for fwrite(), file_put_contents() etc. + * + * @param string $data + * The string to be written. + * + * @return int + * The number of bytes written (integer). + * + * @see http://php.net/manual/en/streamwrapper.stream-write.php + */ + public function stream_write($data) { + // Sanitize the data in a simple way since we're putting it into the + // session variable. + $data = Html::escape($data); + $this->sessionContent = substr_replace($this->sessionContent, $data, $this->streamPointer); + $this->streamPointer += strlen($data); + return strlen($data); + } + + /** + * Support for feof(). + * + * @return bool + * TRUE if end-of-file has been reached. + * + * @see http://php.net/manual/en/streamwrapper.stream-eof.php + */ + public function stream_eof() { + return FALSE; + } + + /** + * Support for fseek(). + * + * @param int $offset + * The byte offset to got to. + * @param int $whence + * SEEK_SET, SEEK_CUR, or SEEK_END. + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.stream-seek.php + */ + public function stream_seek($offset, $whence = SEEK_SET) { + if (strlen($this->sessionContent) >= $offset) { + $this->streamPointer = $offset; + return TRUE; + } + return FALSE; + } + + /** + * Support for fflush(). + * + * @return bool + * TRUE if data was successfully stored (or there was no data to store). + * This always returns TRUE, as this example provides and needs no + * flush support. + * + * @see http://php.net/manual/en/streamwrapper.stream-flush.php + */ + public function stream_flush() { + if ($this->streamMode == 'w') { + // Since we aren't writing directly to the session, we need to send + // the bytes on to the store. + $helper = $this->getSessionWrapper(); + $path = $this->getLocalPath($this->uri); + $helper->setPath($path, $this->sessionContent); + $this->sessionContent = ''; + $this->streamPointer = 0; + } + return TRUE; + } + + /** + * Support for ftell(). + * + * @return int + * The current offset in bytes from the beginning of file. + * + * @see http://php.net/manual/en/streamwrapper.stream-tell.php + */ + public function stream_tell() { + return $this->streamPointer; + } + + /** + * Support for fstat(). + * + * @return array + * An array with file status, or FALSE in case of an error - see fstat() + * for a description of this array. + * + * @see http://php.net/manual/en/streamwrapper.stream-stat.php + */ + public function stream_stat() { + return [ + 'size' => strlen($this->sessionContent), + ]; + } + + /** + * Support for fclose(). + * + * @return bool + * TRUE if stream was successfully closed. + * + * @see http://php.net/manual/en/streamwrapper.stream-close.php + */ + public function stream_close() { + $this->streamPointer = 0; + // Unassign the reference. + unset($this->sessionContent); + return TRUE; + } + + /** + * Support for unlink(). + * + * @param string $uri + * A string containing the uri to the resource to delete. + * + * @return bool + * TRUE if resource was successfully deleted. + * + * @see http://php.net/manual/en/streamwrapper.unlink.php + */ + public function unlink($uri) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + $helper->clearPath($path); + return TRUE; + } + + /** + * Support for rename(). + * + * @param string $from_uri + * The uri to the file to rename. + * @param string $to_uri + * The new uri for file. + * + * @return bool + * TRUE if file was successfully renamed. + * + * @see http://php.net/manual/en/streamwrapper.rename.php + */ + public function rename($from_uri, $to_uri) { + // We get the old key contents, write it + // to a new key, erase the old key. + $from_path = $this->getLocalPath($from_uri); + $to_path = $this->getLocalPath($to_uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($from_path)) { + return FALSE; + } + $from_key = $helper->getPath($from_path); + $path_info = $helper->getParentPath($to_path); + $parent_path = $path_info['dirname']; + $new_file = $path_info['basename']; + // We will only allow writing to a non-existent file + // in an existing directory. + if ($helper->checkPath($parent_path) && !$helper->checkPath($to_path)) { + $helper->setPath($to_path, $from_key); + $helper->clearPath($from_path); + return TRUE; + } + return FALSE; + } + + /** + * Gets the name of the directory from a given path. + * + * @param string $uri + * A URI. + * + * @return string + * A string containing the directory name. + * + * @see drupal_dirname() + */ + public function dirname($uri = NULL) { + list($scheme, $target) = explode('://', $uri, 2); + $target = $this->getTarget($uri); + if (strpos($target, '/')) { + $dirname = preg_replace('@/[^/]*$@', '', $target); + } + else { + $dirname = ''; + } + return $scheme . '://' . $dirname; + } + + /** + * Support for mkdir(). + * + * @param string $uri + * A string containing the URI to the directory to create. + * @param int $mode + * Permission flags - see mkdir(). + * @param int $options + * A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE. + * + * @return bool + * TRUE if directory was successfully created. + * + * @see http://php.net/manual/en/streamwrapper.mkdir.php + */ + public function mkdir($uri, $mode, $options) { + // If this already exists, then we can't mkdir. + if (is_dir($uri) || is_file($uri)) { + return FALSE; + } + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + $new_dir = ['isadir.txt' => TRUE]; + $helper->setPath($path, $new_dir); + return TRUE; + } + + /** + * Support for rmdir(). + * + * @param string $uri + * A string containing the URI to the directory to delete. + * @param int $options + * A bit mask of STREAM_REPORT_ERRORS. + * + * @return bool + * TRUE if directory was successfully removed. + * + * @see http://php.net/manual/en/streamwrapper.rmdir.php + */ + public function rmdir($uri, $options) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path) or !is_array($helper->getPath($path))) { + return FALSE; + } + $helper->clearPath($path); + return TRUE; + } + + /** + * Support for stat(). + * + * This important function goes back to the Unix way of doing things. + * In this example almost the entire stat array is irrelevant, but the + * mode is very important. It tells PHP whether we have a file or a + * directory and what the permissions are. All that is packed up in a + * bitmask. This is not normal PHP fodder. + * + * @param string $uri + * A string containing the URI to get information about. + * @param int $flags + * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET. + * + * @return array|bool + * An array with file status, or FALSE in case of an error - see fstat() + * for a description of this array. + * + * @see http://php.net/manual/en/streamwrapper.url-stat.php + */ + public function url_stat($uri, $flags) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path)) { + return FALSE; + // No file. + } + // Default to fail. + $return = FALSE; + $mode = 0; + + $path_info = $helper->getParentPath($path); + $key = $helper->getPath($path); + $key_name = $path_info['basename']; + // We will call an array a directory and the root is always an array. + if (is_array($key)) { + // S_IFDIR means it's a directory. + $mode = 0040000; + } + elseif ($key !== FALSE) { + // S_IFREG, means it's a file. + $mode = 0100000; + } + + if ($mode) { + $size = 0; + if ($mode == 0100000) { + $size = strlen($key); + } + + // There are no protections on this, so all writable. + $mode |= 0777; + $return = [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $mode, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $size, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + ]; + } + return $return; + } + + /** + * Support for opendir(). + * + * @param string $uri + * A string containing the URI to the directory to open. + * @param int $options + * Whether or not to enforce safe_mode (0x04). + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-opendir.php + */ + public function dir_opendir($uri, $options) { + $path = $this->getLocalPath($uri); + $helper = $this->getSessionWrapper(); + if (!$helper->checkPath($path)) { + return FALSE; + } + $var = $helper->getPath($path); + if (!is_array($var)) { + return FALSE; + } + + // We grab the list of key names, flip it so that .isadir.txt can easily + // be removed, then flip it back so we can easily walk it as a list. + $this->directoryKeys = array_flip(array_keys($var)); + unset($this->directoryKeys['.isadir.txt']); + $this->directoryKeys = array_keys($this->directoryKeys); + $this->directoryPointer = 0; + return TRUE; + } + + /** + * Support for readdir(). + * + * @return string|bool + * The next filename, or FALSE if there are no more files in the directory. + * + * @see http://php.net/manual/en/streamwrapper.dir-readdir.php + */ + public function dir_readdir() { + if ($this->directoryPointer < count($this->directoryKeys)) { + $next = $this->directoryKeys[$this->directoryPointer]; + $this->directoryPointer++; + return $next; + } + return FALSE; + } + + /** + * Support for rewinddir(). + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-rewinddir.php + */ + public function dir_rewinddir() { + $this->directoryPointer = 0; + return TRUE; + } + + /** + * Support for closedir(). + * + * @return bool + * TRUE on success. + * + * @see http://php.net/manual/en/streamwrapper.dir-closedir.php + */ + public function dir_closedir() { + $this->directoryPointer = 0; + unset($this->directoryKeys); + return TRUE; + } + +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php new file mode 100644 index 000000000..ba59b0d49 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/MockSessionTrait.php @@ -0,0 +1,100 @@ +sessionStore = []; + $session = $this->prophesize(SessionInterface::class); + $test = $this; + + $session + ->get('stream_wrapper_example', []) + ->will(function ($args) use ($test) { + return $test->getSessionStore(); + }); + + $session + ->set('stream_wrapper_example', Argument::any()) + ->will(function ($args) use ($test) { + $test->setSessionStore($args[1]); + }); + + $session + ->remove('stream_wrapper_example') + ->will(function ($args) use ($test) { + $test->resetSessionStore(); + }); + + $request = $this->prophesize(Request::class); + $request + ->getSession() + ->willReturn($session->reveal()); + + $request_stack = $this->prophesize(RequestStack::class); + $request_stack + ->getCurrentRequest() + ->willReturn($request->reveal()); + + return $this->requestStack = $request_stack->reveal(); + } + + /** + * Get a session wrapper. + */ + public function getSessionWrapper() { + return new SessionWrapper($this->requestStack); + } + + /** + * Helper for mocks. + */ + public function getSessionStore() { + return $this->sessionStore; + } + + /** + * Helper for our mocks. + */ + public function setSessionStore($data) { + $this->sessionStore = $data; + } + + /** + * Helper for our mocks. + */ + public function resetSessionStore() { + $this->sessionStore = []; + } + +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php new file mode 100644 index 000000000..a540a5670 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/src/StreamWrapper/SessionWrapper.php @@ -0,0 +1,247 @@ +requestStack = $request_stack; + $this->storePath = ''; + } + + /** + * Get a fresh session object. + * + * @return \Symfony\Component\HttpFoundation\Session\SessionInterface + * A session object. + */ + protected function getSession() { + return $this->requestStack->getCurrentRequest()->getSession(); + } + + /** + * Get whatever's in the store. + * + * @return array + * An associated array where scalar data represents a file, and arrays + * represent directories. + */ + protected function getStore() { + $session = $this->getSession(); + $store = $session->get(static::SESSION_BASE_ATTRIBUTE, []); + return $store; + } + + /** + * Set the complete content of our session. + * + * We do this since we cannot directly probe the PHP @_SESSION variable + * via a PHP reference. We can only get or set automically. + * + * @param array $store + * The content of the whole session data store, to replace all of the + * current data. + */ + protected function setStore(array $store) { + $session = $this->getSession(); + $session->set(static::SESSION_BASE_ATTRIBUTE, $store); + } + + /** + * Turn a path into the arrays we use internally. + * + * @param string $path + * Path into the store. + * @param bool $is_dir + * Path will be used as a container. Otherwise, path is a scalar. + * + * @return array|bool + * Return an array containing the "bottom" and "tip" of a directory + * hierarchy. You will want to save the 'bottom' array, but you may + * need to manipulate an object at the very tip of the hierarchy + * as defined in the path. The tip will be a string if we are scalar + * and an array otherwise. Since we don't want to create new + * sub arrays as a side effect, we return FALSE the intervening path + * does not exist. + */ + public function processPath($path, $is_dir = FALSE) { + // We need to create a reference into the store for the point + // the of the path, so get a copy of the store. + $store = $this->getStore(); + + if (empty($path)) { + return ['store' => &$store, 'tip' => &$store]; + } + $hierarchy = explode('/', $path); + if (empty($hierarchy) or empty($hierarchy[0])) { + return ['store' => &$store, 'tip' => &$store]; + } + $bottom =& $store; + $tip = array_pop($hierarchy); + + foreach ($hierarchy as $dir) { + if (!isset($bottom[$dir])) { + // If the path does not exist, DO NOT create it. + // That is handled by the stream wrapper code. + return FALSE; + } + $new_tip =& $bottom[$dir]; + $bottom =& $new_tip; + } + // If the hierarchy was empty, just point to the object. + $new_tip =& $bottom[$tip]; + $bottom =& $new_tip; + return ['store' => &$store, 'tip' => &$bottom]; + } + + /** + * The equivalent to dirname() and basename() for a path. + * + * @param string $path + * A file-system like path string. + * + * @return array + * Associative array defining an interal path of our data store. . + */ + public function getParentPath($path) { + $dirs = explode('/', $path); + $tip = array_pop($dirs); + $parent = implode('/', $dirs); + return ['dirname' => $parent, 'basename' => $tip]; + } + + /** + * Clear a path into our store. + * + * @param string $path + * The path portion of a URI (i.e., without the SCHEME://). + */ + public function clearPath($path) { + $store = $this->getStore(); + if ($this->checkPath($path)) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath($path_info['dirname']); + if ($store_info === FALSE) { + // The path was not found, nothing to do. + return; + + } + // We want to clear the key at the tip, so... + unset($store_info['tip'][$path_info['basename']]); + // Write back to the store. + $this->setStore($store_info['store']); + } + + } + + /** + * Get a path. + * + * @param string $path + * A URI with the SCHEME:// part removed. + * + * @return mixed + * Return the stored value at this "node" of the store. + */ + public function getPath($path) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath(($path_info['dirname'])); + $leaf = $path_info['basename']; + if ($store_info === FALSE) { + return NULL; + } + if ($store_info['store'] === $store_info['tip']) { + // We are at the top of the hierarchy; return the store itself. + if (empty($path_info['basename'])) { + return $store_info['store']; + } + if (!isset($store_info['store'][$leaf])) { + return NULL; + } + } + return $store_info['tip'][$leaf]; + } + + /** + * Set a path. + * + * @param string $path + * Path into the store. + * @param string|array $value + * Set a value. + */ + public function setPath($path, $value) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath(($path_info['dirname'])); + if ($store_info !== FALSE) { + $store_info['tip'][$path_info['basename']] = $value; + } + $this->setStore($store_info['store']); + } + + /** + * Does path exist? + * + * @param string $path + * Path into the store. + */ + public function checkPath($path) { + $path_info = $this->getParentPath($path); + $store_info = $this->processPath($path_info['dirname']); + if (empty($store_info)) { + // Containing directory did not exist. + return FALSE; + } + // Check if we are at the root of a directory. + if ($path_info['basename'] === '') { + return TRUE; + } + return isset($store_info['tip'][$path_info['basename']]); + } + + /** + * Set up the store for use. + */ + public function setUpStore() { + // Nothing to do with $_SESSION version. + } + + /** + * Zero out the store. + */ + public function cleanUpStore() { + $session = $this->getSession(); + $session->remove(static::SESSION_BASE_ATTRIBUTE); + } + +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.info.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.info.yml new file mode 100644 index 000000000..f9ec16409 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.info.yml @@ -0,0 +1,13 @@ +name: Stream Wrapper Example +type: module +description: Example of implementing Stream Wrappers in Drupal. +package: Example modules +# core: 8.x +dependencies: + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.links.menu.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.links.menu.yml new file mode 100644 index 000000000..47fdf2bba --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.links.menu.yml @@ -0,0 +1,4 @@ +# Menu links for the "Tools" menu. +stream_wrapper_example.description: + title: Stream Wrapper Example + route_name: stream_wrapper_example.description diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.module b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.module new file mode 100644 index 000000000..18309d9ac --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.module @@ -0,0 +1,108 @@ + [ + 'template' => 'description', + 'variables' => [ + 'admin_link' => NULL, + ], + ], + ]; +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.routing.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.routing.yml new file mode 100644 index 000000000..702c6c35b --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.routing.yml @@ -0,0 +1,51 @@ +# In order to view files created with our demo stream wrapper class, +# we need to use hook_file_download to grant any access. This route +# will make sure that we have an external URL for these files, and that +# our hook is called. +# +# In our implementation, access to the files is actually managed by +# permissions defined in file_example.permissions.yml. Since we also want our +# URLs to be served similar to how private: and temporary: URI are served by +# core, we also need to modify how the routing system handles the tail portion +# of the URL. Unlike Drupal 7, Drupal 8 does not ordinarily allow a "menu tail"; +# URLs need to be of a definite length or the router will not process them. To +# get around this, we also implement a "path processor", which we define as a +# service in our services file. Our path processor will do the extra steps needed +# to process our session file URLs. +# +# @see stream_wrapper_example.services.yml +# @see file_example_file_download() +# +stream_wrapper_example.files: + path: '/examples/stream_wrapper_example/files/{scheme}' + defaults: + _controller: 'Drupal\system\FileDownloadController::download' + scheme: session + requirements: + _access: 'TRUE' + +# In addition to the stream_wrapper_example.files route, which is actually matched by the router, +# we also need a route defintion to make our URLs. This is never referenced by the +# routing system, but is used by our stream wrapper class to create external URLs. +# +# @see FileExampleSessionStreamWrapper::getExternalUrl() +# +stream_wrapper_example.files.session: + path: '/examples/stream_wrapper_example/files/{filepath}' + defaults: + _controller: '\Drupal\system\FileDownloadController::download' + scheme: session + requirements: + # Permissive regex to allow slashes in filepath see + # http://symfony.com/doc/current/cookbook/routing/slash_in_parameter.html + filepath: .+ + _access: 'TRUE' + +# Finally, our controller class. +stream_wrapper_example.description: + path: '/examples/stream_wrapper_example' + defaults: + _controller: '\Drupal\stream_wrapper_example\Controller\StreamWrapperExampleController::description' + _title: 'Stream Wrapper Example' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.services.yml b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.services.yml new file mode 100644 index 000000000..b6b2db39d --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/stream_wrapper_example.services.yml @@ -0,0 +1,34 @@ +# +# As part of our demo, we implement a simple "file system" that lets us read and write +# files out of the $_SESSION. This isn't very practical, but it's a simple way to +# demonstrate what you can do with PHP's stream wrappers. +# +# To get a stream wrapper to work to define a stream wrapper class, we need to register +# that with the system. We can either do this manually by calling up the 'stream_wrapper.manager' +# service, but the better way to do this is to have the system autoload it by tagging the service, +# as we do here. +# +# We also want to securely serve up our fake session files. We'd like to use the same nice +# file paths that Core uses for private files. Since Drupal 8 no longer allows us to have +# "menu tails" (i.e., extra/parts/of/the/path after the default part of the path), we need +# to get some router superpowers. Our route (in stream_wrapper_example.routing.yml) will "gather up" +# the path with with a regular expression. But we need to do a little more that that. We +# also need to convince the routing system to see our weird, extra long route route. We +# do that using a "Path Processor". We register the path_process.sessions service with special +# tags to get it loaded for when the Drupal's routing system decides which path should get +# used. +# +# @see src/StreamWrapper/FileExampleSessionStreamWrapper.php +# @see src/PathProcessor/PathProcessorSessions.php +# @see stream_wrapper_example.routing.yml +# +services: + stream_wrapper_example.stream_wrapper: + class: Drupal\stream_wrapper_example\StreamWrapper\FileExampleSessionStreamWrapper + tags: + - { name: stream_wrapper, scheme: session } + + path_processor.sessions: + class: Drupal\stream_wrapper_example\PathProcessor\PathProcessorSessions + tags: + - { name: path_processor_inbound, priority: 200 } diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/templates/description.html.twig b/sites/all/modules/examples/examples/stream_wrapper_example/templates/description.html.twig new file mode 100644 index 000000000..d06dd8cdb --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/templates/description.html.twig @@ -0,0 +1,44 @@ +{# +/** + * @file + * Contains the description text of an Example explanation/description page + * + * Available variables: + * - admin_link: The translated link pointing to a configuration page for the example. + */ +#} + +
    + + {% trans %} +

    The Stream Wrapper Example module demonstrates a PHP stream wrapper implementation. + A stream wrapper is a class that implements something that looks and behaves like a + file system. A particular implementation of a stream wrapper is called a scheme. + Drupal 8 supports public, private, and temporary wrapper schemes. For example, you + access a file in your public uploads directory via a "public" file URI such as + public://images/big-logo.png. When you read, write, delete or move that + file, the public scheme's stream wrapper class + (\Drupal\Core\StreamWrapper\PublicStream) is invoked to do the reading, + writing, deletion or moving. PHP does this automatically for you, creating the wrapper + whenever some file operation needs to get done on a public:// file. +

    + +

    To demonstrate how to implement a stream wrapper, this example module creates a + session wrapper scheme. It uses your session data (created when you + log into Drupal) to create a nested array where the arrays represent directories, + and scalar values represent files. This is completely impractical, and frankly, + not terribly secure, so you should never enable this module on any site that's + open to the Internet. But without using any special libraries, our stream wrapper + class is able to create and delete directories, and read and write files. +

    + +

    If you want to play with session file URIs, we recommend also enabling + the File Example (file_example.module), which will let you do the same things with + the "session" scheme that you can do with public, private or temporary files.

    + +

    A longer description of what code is where can be found in + stream_wrapper_example.module. Definitely look through the code to see + various implementation details.

    + {% endtrans %} + +
    \ No newline at end of file diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php new file mode 100644 index 000000000..6441fb3c1 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Kernel/StreamWrapperTest.php @@ -0,0 +1,159 @@ +root)); + parent::setUp(); + // Typically if we need our tested class to get information from the system, + // we use dependency injection (DI) to get that information to the class. + // But stream wrappers are unusual. They are created automatically by PHP + // itself when it calls one of the standard file functions, and for that + // reason, the constructor functions of stream wrappers cannot be passed any + // arguments, which prevents us from using the stardard DI technique we use + // in Drupal 8. The alternative is to create a "global" container that makes + // our services available to the class, which is what we do here. + $request_stack = $this->createSessionMock(); + $this->container->set('request_stack', $request_stack); + $this->container->set('file_system', \Drupal::service('file_system')); + $this->container->set('kernel', \Drupal::service('kernel')); + \Drupal::setContainer($this->container); + } + + /** + * Test if the session scheme was actually registered. + */ + public function testSchemeRegistered() { + $have_session_scheme = $this->container->get('file_system')->validScheme('session'); + $this->assertTrue($have_session_scheme, "System knows about our stream wrapper"); + } + + /** + * Test functions on a URI. + */ + public function testReadWrite() { + $this->resetStore(); + $store = $this->getCurrentStore(); + + $uri = 'session://drupal.txt'; + + $this->assertFalse(file_exists($uri), "File $uri should not exist yet."); + $handle = fopen($uri, 'wb'); + $this->assertNotEmpty($handle, "Handle for $uri should be non-empty."); + $buffer = "Ain't seen nothin' yet!\n"; + $len = strlen($buffer); + + // Original session class gets an error here, + // "...stream_write wrote 10 bytes more data than requested". + // Does not matter for our demo, so repress error reporting here.". + $old = error_reporting(E_ERROR); + $bytes_written = @fwrite($handle, $buffer); + error_reporting($old); + $this->assertNotFalse($bytes_written, "Write to $uri succeeded."); + + $rslt = fclose($handle); + $this->assertNotFalse($rslt, "Closed $uri."); + $this->assertTrue(file_exists($uri), "File $uri should now exist."); + $this->assertFalse(is_dir($uri), "$uri is not a directory."); + $this->assertTrue(is_file($uri), "$uri is a file."); + $size = filesize($uri); + + $contents = file_get_contents($uri); + // The example implementation calls HTML::escape() on output. We reverse it + // well enough for our sample data (this code is not I18n safe). + $contents = Html::decodeEntities($contents); + $this->assertEquals($buffer, $contents, "Data for $uri should make the round trip."); + } + + /** + * Directory creation. + */ + public function testDirectories() { + $this->resetStore(); + $dir_uri = 'session://directory1/directory2'; + $sample_file = 'file.txt'; + $content = "Wrote this as a file?\n"; + $dir2 = basename($dir_uri); + $dir1 = dirname($dir_uri); + + $this->assertFalse(file_exists($dir1), "The outer dir $dir1 should not exist yet."); + // We don't care about mode, since we don't support it. + $worked = mkdir($dir1); + $this->assertTrue(is_dir($dir1), "Directory $dir1 was created."); + $first_file_content = "This one is in the first directory."; + $uri = $dir1 . "/" . $sample_file; + $bytes = file_put_contents($uri, $first_file_content); + $this->assertNotFalse($bytes, "Wrote to $uri.\n"); + $this->assertTrue(file_exists($uri), "File $uri actually exists."); + $got_back = file_get_contents($uri); + $got_back = Html::decodeEntities($got_back); + $this->assertSame($first_file_content, $got_back, "Data in subdir made round trip."); + + // Now try down down nested. + $rslt = mkdir($dir_uri); + $this->assertTrue($rslt, "Nested dir got created."); + $file_in_sub = $dir_uri . "/" . $sample_file; + $bytes = file_put_contents($file_in_sub, $content); + $this->assertNotFalse($bytes, "File in nested dirs got written to."); + $got_back = file_get_contents($file_in_sub); + $got_back = Html::decodeEntities($got_back); + $this->assertSame($content, $got_back, "Data in subdir made round trip."); + $worked = unlink($file_in_sub); + $this->assertTrue($worked, "Deleted file in subdir."); + $this->assertFalse(file_exists($file_in_sub), "File in subdir should not exist."); + } + + /** + * Get the contents of the complete array stored in the session. + */ + protected function getCurrentStore() { + $handle = $this->getSessionWrapper(); + return $handle->getPath(''); + } + + /** + * Clear the session storage area. + */ + protected function resetStore() { + $handle = $this->getSessionWrapper(); + $handle->cleanUpStore(); + } + +} diff --git a/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php new file mode 100644 index 000000000..be52ef721 --- /dev/null +++ b/sites/all/modules/examples/examples/stream_wrapper_example/tests/src/Unit/SessionWrapperTest.php @@ -0,0 +1,101 @@ +createSessionMock(); + + // Set up the example. + $helper = new SessionWrapper($this->requestStack); + $helper->setUpStore(); + } + + /** + * Run our wrapper through the paces. + */ + public function testWrapper() { + // Check out root. + $helper = new SessionWrapper($this->requestStack); + $root = $helper->getPath(''); + $this->assertTrue(is_array($root), "The root is an array"); + $this->assertTrue(empty($root), "The root is empty."); + + // Add a top level file. + $helper = new SessionWrapper($this->requestStack); + $helper->setPath('drupal.txt', "Stuff"); + $text = $helper->getPath('drupal.txt'); + $this->assertEquals($text, "Stuff", "File at base of hierarchy can be read."); + + // Add a "directory". + $helper = new SessionWrapper($this->requestStack); + $dir = [ + 'file.txt' => 'More stuff', + ]; + $helper->setPath('directory1', $dir); + $fetched_dir = $helper->getPath('directory1'); + $this->assertEquals($fetched_dir['file.txt'], "More stuff", "File inside of directory can be read."); + + // Check file existance. + $helper = new SessionWrapper($this->requestStack); + $this->assertTrue($helper->checkPath('drupal.txt'), "File at root still exists."); + $this->assertFalse($helper->checkPath('file.txt'), "Non-existant file at root does not exist."); + $this->assertTrue($helper->checkPath('directory1'), "Directory at root still exists."); + $this->assertTrue($helper->checkPath('directory1/file.txt'), "File in directory at root still exists."); + + // Two deep. + $helper = new SessionWrapper($this->requestStack); + $helper->setPath('directory1/directory2', []); + $helper->setPath('directory1/directory2/junk.txt', "Store some junk"); + $text = $helper->getPath('directory1/directory2/junk.txt'); + $this->assertEquals($text, "Store some junk", "File inside of nested directory can be read."); + + // Clear references. + $helper = new SessionWrapper($this->requestStack); + $before = $helper->checkPath('directory1/directory2/junk.txt'); + $this->assertTrue($before, "File 2 deep exists."); + $helper->clearPath('directory1/directory2/junk.txt'); + $after = $helper->checkPath('directory1/directory2/junk.txt'); + $this->assertFalse($after, "File 2 deep should be gone."); + + // Clean up test. + $helper = new SessionWrapper($this->requestStack); + $store = $helper->getPath(''); + $this->assertNotEmpty($store, "Before cleanup store is not empty."); + $helper->cleanUpStore(); + $store = $helper->getPath(''); + $this->assertEmpty($store, "After cleanup store is empty."); + + } + +} diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Controller/TableDragExampleController.php b/sites/all/modules/examples/examples/tabledrag_example/src/Controller/TableDragExampleController.php new file mode 100644 index 000000000..a3d7cea42 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/src/Controller/TableDragExampleController.php @@ -0,0 +1,24 @@ + 'table', + '#header' => [ + $this->t('Name'), + $this->t('Description'), + $this->t('Weight'), + $this->t('Parent'), + ], + '#empty' => $this->t('Sorry, There are no items!'), + // TableDrag: Each array value is a list of callback arguments for + // drupal_add_tabledrag(). The #id of the table is automatically + // prepended; if there is none, an HTML ID is auto-generated. + '#tabledrag' => [ + [ + 'action' => 'match', + 'relationship' => 'parent', + 'group' => 'row-pid', + 'source' => 'row-id', + 'hidden' => TRUE, /* hides the WEIGHT & PARENT tree columns below */ + 'limit' => FALSE, + ], + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'row-weight', + ], + ], + ]; + + // Build the table rows and columns. + // + // The first nested level in the render array forms the table row, on which + // you likely want to set #attributes and #weight. + // Each child element on the second level represents a table column cell in + // the respective table row, which are render elements on their own. For + // single output elements, use the table cell itself for the render element. + // If a cell should contain multiple elements, simply use nested sub-keys to + // build the render element structure for drupal_render() as you would + // everywhere else. + $results = self::getData(); + foreach ($results as $row) { + // TableDrag: Mark the table row as draggable. + $form['table-row'][$row->id]['#attributes']['class'][] = 'draggable'; + + // Indent item on load. + if (isset($row->depth) && $row->depth > 0) { + $indentation = [ + '#theme' => 'indentation', + '#size' => $row->depth, + ]; + } + + // Some table columns containing raw markup. + $form['table-row'][$row->id]['name'] = [ + '#markup' => $row->name, + '#prefix' => !empty($indentation) ? drupal_render($indentation) : '', + ]; + + $form['table-row'][$row->id]['description'] = [ + '#type' => 'textfield', + '#required' => TRUE, + '#default_value' => $row->description, + ]; + + // This is hidden from #tabledrag array (above). + // TableDrag: Weight column element. + $form['table-row'][$row->id]['weight'] = [ + '#type' => 'weight', + '#title' => $this->t('Weight for ID @id', ['@id' => $row->id]), + '#title_display' => 'invisible', + '#default_value' => $row->weight, + // Classify the weight element for #tabledrag. + '#attributes' => [ + 'class' => ['row-weight'], + ], + ]; + $form['table-row'][$row->id]['parent']['id'] = [ + '#parents' => ['table-row', $row->id, 'id'], + '#type' => 'hidden', + '#value' => $row->id, + '#attributes' => [ + 'class' => ['row-id'], + ], + ]; + $form['table-row'][$row->id]['parent']['pid'] = [ + '#parents' => ['table-row', $row->id, 'pid'], + '#type' => 'number', + '#size' => 3, + '#min' => 0, + '#title' => $this->t('Parent ID'), + '#default_value' => $row->pid, + '#attributes' => [ + 'class' => ['row-pid'], + ], + ]; + } + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save All Changes'), + ]; + $form['actions']['cancel'] = [ + '#type' => 'submit', + '#value' => 'Cancel', + '#attributes' => [ + 'title' => $this->t('Return to TableDrag Overview'), + ], + '#submit' => ['::cancel'], + '#limit_validation_errors' => [], + ]; + + return $form; + } + + /** + * Form submission handler for the 'Return to' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function cancel(array &$form, FormStateInterface $form_state) { + $form_state->setRedirect('tabledrag_example.description'); + } + + /** + * Submit handler for the form. + * + * Updates the 'weight' column for each element in our table, taking into + * account that item's new order after the drag and drop actions have been + * performed. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Because the form elements were keyed with the item ids from the database, + // we can simply iterate through the submitted values. + $submissions = $form_state->getValue('table-row'); + foreach ($submissions as $id => $item) { + db_update('tabledrag_example') + ->fields([ + 'weight' => $item['weight'], + 'pid' => $item['pid'], + 'description' => $item['description'], + ]) + ->condition('id', $id, '=') + ->execute(); + } + } + + /** + * Retrieves the tree structure from db and sorts by parent/child/weight. + * + * The sorting should result in children items immediately following their + * parent items, with items at the same level of the hierarchy sorted by + * weight. + * + * The approach used here may be considered too database-intensive. + * Optimization of the approach is left as an exercise for the reader. :) + * + * @return array + * An associative array storing our ordered tree structure. + */ + public function getData() { + // Get all 'root node' items (items with no parents), sorted by weight. + $root_items = db_select('tabledrag_example', 't') + ->fields('t') + ->condition('pid', '0', '=') + ->condition('id', 11, '<') + ->orderBy('weight') + ->execute() + ->fetchAll(); + + // Initialize a variable to store our ordered tree structure. + $tree = []; + + // Depth will be incremented in our getTree() + // function for the first parent item, so we start it at -1. + $depth = -1; + + // Loop through the root item, and add their trees to the array. + foreach ($root_items as $root_item) { + $this->getTree($root_item, $tree, $depth); + } + + return $tree; + } + + /** + * Recursively adds $item to $item_tree, ordered by parent/child/weight. + * + * @param mixed $item + * The item. + * @param array $tree + * The item tree. + * @param int $depth + * The depth of the item. + */ + public function getTree($item, array &$tree = [], &$depth = 0) { + // Increase our $depth value by one. + $depth++; + + // Set the current tree 'depth' for this item, used to calculate + // indentation. + $item->depth = $depth; + + // Add the item to the tree. + $tree[$item->id] = $item; + + // Retrieve each of the children belonging to this nested demo. + $children = db_select('tabledrag_example', 't') + ->fields('t') + ->condition('pid', $item->id, '=') + ->condition('id', 11, '<') + ->orderBy('weight') + ->execute() + ->fetchAll(); + + foreach ($children as $child) { + // Make sure this child does not already exist in the tree, to + // avoid loops. + if (!in_array($child->id, array_keys($tree))) { + // Add this child's tree to the $itemtree array. + $this->getTree($child, $tree, $depth); + } + } + + // Finished processing this tree branch. Decrease our $depth value by one + // to represent moving to the next branch. + $depth--; + } + +} diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleResetForm.php b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleResetForm.php new file mode 100644 index 000000000..0dfc16464 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleResetForm.php @@ -0,0 +1,97 @@ +t('Reset demo data for TableDrag Example'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('tabledrag_example.description'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('Are you sure you want to reset demo data?'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Yes, Reset It!'); + } + + /** + * {@inheritdoc} + */ + public function getCancelText() { + return $this->t('Cancel'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Load tabledrag_example.install so that we can call + // tabledrag_example_data(). + module_load_include('inc', 'tabledrag_example', 'tabledrag_example.data'); + $data = tabledrag_example_data(); + foreach ($data as $id => $item) { + // Add 1 to each array key to match ID. + $id++; + db_update('tabledrag_example') + ->fields([ + 'weight' => 0, + 'pid' => 0, + 'description' => $item['description'], + 'itemgroup' => $item['itemgroup'], + ]) + ->condition('id', $id, '=') + ->execute(); + } + drupal_set_message($this->t('Data for TableDrag Example has been reset.'), 'status'); + $form_state->setRedirect('tabledrag_example.description'); + } + +} diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleRootLeafForm.php b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleRootLeafForm.php new file mode 100644 index 000000000..edf0269e2 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleRootLeafForm.php @@ -0,0 +1,309 @@ + 'item', + '#markup' => $this->t('Tabledrag rows can be marked as roots or leaves. This limits the way the user can interact with them in drag-and-drop operations. We\'ll mark some rows this way and you can try dragging them around on the page to see how they are limited.'), + ]; + + $form['info'] = [ + '#markup' => '
      +
    • ' . $this->t("Rows with the 'tabledrag-leaf' class cannot have child rows.") . '
    • +
    • ' . $this->t("Rows with the 'tabledrag-root' class cannot be nested under a parent row.") . '
    ', + ]; + + $form['table-row'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Name'), + $this->t('Description'), + $this->t('Weight'), + $this->t('Parent'), + ], + '#empty' => $this->t('Sorry, There are no items!'), + // TableDrag: Each array value is a list of callback arguments for + // drupal_add_tabledrag(). The #id of the table is automatically + // prepended; if there is none, an HTML ID is auto-generated. + '#tabledrag' => [ + [ + 'action' => 'match', + 'relationship' => 'parent', + 'group' => 'row-pid', + 'source' => 'row-id', + 'hidden' => TRUE, /* hides the WEIGHT & PARENT tree columns below */ + 'limit' => FALSE, + ], + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'row-weight', + ], + ], + ]; + + // Build the table rows and columns. + // + // The first nested level in the render array forms the table row, on which + // you likely want to set #attributes and #weight. + // Each child element on the second level represents a table column cell in + // the respective table row, which are render elements on their own. For + // single output elements, use the table cell itself for the render element. + // If a cell should contain multiple elements, simply use nested sub-keys to + // build the render element structure for drupal_render() as you would + // everywhere else. + $results = self::getData(); + foreach ($results as $row) { + // TableDrag: Mark the table row as draggable. + $form['table-row'][$row->id]['#attributes']['class'][] = 'draggable'; + + // We can add the 'tabledrag-root' class to a row in order to indicate + // that the row may not be nested under a parent row. In our sample data + // for this example, the description for the item with id '11' flags it as + // a 'root' item which should not be nested. + if ($row->id == '11') { + $form['table-row'][$row->id]['#attributes']['class'][] = 'tabledrag-root'; + } + + // We can add the 'tabledrag-leaf' class to a row in order to indicate + // that the row may not contain child rows. In our sample data for this + // example, the description for the item with id '12' flags it as a 'leaf' + // item which can not contain child items. + if ($row->id == '12') { + $form['table-row'][$row->id]['#attributes']['class'][] = 'tabledrag-leaf'; + } + + // TableDrag: Sort the table row according to its existing/configured + // weight. + $form['table-row'][$row->id]['#weight'] = $row->weight; + + // Indent item on load. + if (isset($row->depth) && $row->depth > 0) { + $indentation = [ + '#theme' => 'indentation', + '#size' => $row->depth, + ]; + } + // Some table columns containing raw markup. + $form['table-row'][$row->id]['name'] = [ + '#markup' => $row->name, + '#prefix' => !empty($indentation) ? drupal_render($indentation) : '', + ]; + + $form['table-row'][$row->id]['description'] = [ + '#type' => 'textfield', + '#required' => TRUE, + '#default_value' => $row->description, + ]; + + // This is hidden from #tabledrag array (above). + // TableDrag: Weight column element. + $form['table-row'][$row->id]['weight'] = [ + '#type' => 'weight', + '#title' => $this->t('Weight for ID @id', ['@id' => $row->id]), + '#title_display' => 'invisible', + '#default_value' => $row->weight, + // Classify the weight element for #tabledrag. + '#attributes' => [ + 'class' => ['row-weight'], + ], + ]; + $form['table-row'][$row->id]['parent']['id'] = [ + '#parents' => ['table-row', $row->id, 'id'], + '#type' => 'hidden', + '#value' => $row->id, + '#attributes' => [ + 'class' => ['row-id'], + ], + ]; + $form['table-row'][$row->id]['parent']['pid'] = [ + '#parents' => ['table-row', $row->id, 'pid'], + '#type' => 'number', + '#size' => 3, + '#min' => 0, + '#title' => $this->t('Parent ID'), + '#default_value' => $row->pid, + '#attributes' => [ + 'class' => ['row-pid'], + ], + ]; + } + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save All Changes'), + ]; + $form['actions']['cancel'] = [ + '#type' => 'submit', + '#value' => 'Cancel', + '#attributes' => [ + 'title' => $this->t('Return to TableDrag Overview'), + ], + '#submit' => ['::cancel'], + ]; + + return $form; + } + + /** + * Form submission handler for the 'Return to' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function cancel(array &$form, FormStateInterface $form_state) { + $form_state->setRedirect('tabledrag_example.description'); + } + + /** + * Submit handler for the form. + * + * Updates the 'weight' column for each element in our table, taking into + * account that item's new order after the drag and drop actions have been + * performed. + * + * @param array $form + * Render array representing from. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Current form state. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Because the form elements were keyed with the item ids from the database, + // we can simply iterate through the submitted values. + $submissions = $form_state->getValue('table-row'); + foreach ($submissions as $id => $item) { + db_update('tabledrag_example') + ->fields([ + 'weight' => $item['weight'], + 'pid' => $item['pid'], + 'description' => $item['description'], + ]) + ->condition('id', $id, '=') + ->execute(); + } + } + + /** + * Retrieves the tree structure from database, sorts by parent/child/weight. + * + * The sorting should result in children items immediately following their + * parent items, with items at the same level of the hierarchy sorted by + * weight. + * + * The approach used here may be considered too database-intensive. + * Optimization of the approach is left as an exercise for the reader. :) + * + * @return array + * An associative array storing our ordered tree structure. + */ + public function getData() { + // Get all 'root node' items (items with no parents), sorted by weight. + $root_items = db_select('tabledrag_example', 't') + ->fields('t') + ->condition('pid', '0', '=') + ->orderBy('weight') + ->execute() + ->fetchAll(); + + // Initialize a variable to store our ordered tree structure. + $tree = []; + + // Depth will be incremented in our getTree() + // function for the first parent item, so we start it at -1. + $depth = -1; + + // Loop through the root item, and add their trees to the array. + foreach ($root_items as $root_item) { + $this->getTree($root_item, $tree, $depth); + } + + return $tree; + } + + /** + * Recursively adds $item to $item_tree, ordered by parent/child/weight. + * + * @param mixed $item + * The item. + * @param array $tree + * The item tree. + * @param int $depth + * The depth of the item. + */ + public function getTree($item, array &$tree = [], &$depth = 0) { + // Increase our $depth value by one. + $depth++; + + // Set the current tree 'depth' for this item, used to calculate + // indentation. + $item->depth = $depth; + + // Add the item to the tree. + $tree[$item->id] = $item; + + // Retrieve each of the children belonging to this nested demo. + $children = db_select('tabledrag_example', 't') + ->fields('t') + ->condition('pid', $item->id, '=') + ->orderBy('weight') + ->execute() + ->fetchAll(); + + foreach ($children as $child) { + // Make sure this child does not already exist in the tree, to + // avoid loops. + if (!in_array($child->id, array_keys($tree))) { + // Add this child's tree to the $itemtree array. + $this->getTree($child, $tree, $depth); + } + } + + // Finished processing this tree branch. Decrease our $depth value by one + // to represent moving to the next branch. + $depth--; + } + +} diff --git a/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleSimpleForm.php b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleSimpleForm.php new file mode 100644 index 000000000..891403d1c --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/src/Form/TableDragExampleSimpleForm.php @@ -0,0 +1,154 @@ + 'table', + '#header' => [ + $this->t('Name'), + $this->t('Description'), + $this->t('Weight'), + ], + '#empty' => $this->t('Sorry, There are no items!'), + // TableDrag: Each array value is a list of callback arguments for + // drupal_add_tabledrag(). The #id of the table is automatically + // prepended; if there is none, an HTML ID is auto-generated. + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'table-sort-weight', + ], + ], + ]; + + // Build the table rows and columns. + // + // The first nested level in the render array forms the table row, on which + // you likely want to set #attributes and #weight. + // Each child element on the second level represents a table column cell in + // the respective table row, which are render elements on their own. For + // single output elements, use the table cell itself for the render element. + // If a cell should contain multiple elements, simply use nested sub-keys to + // build the render element structure for drupal_render() as you would + // everywhere else. + // + // About the condition id<8: + // For the purpose of this 'simple table' we are only using the first 8 rows + // of the database. The others are for 'nested' example. + $results = db_select('tabledrag_example', 't') + ->fields('t') + ->orderBy('weight') + ->condition('id', 8, '<') + ->execute() + ->fetchAll(); + foreach ($results as $row) { + // TableDrag: Mark the table row as draggable. + $form['table-row'][$row->id]['#attributes']['class'][] = 'draggable'; + // TableDrag: Sort the table row according to its existing/configured + // weight. + $form['table-row'][$row->id]['#weight'] = $row->weight; + + // Some table columns containing raw markup. + $form['table-row'][$row->id]['name'] = [ + '#markup' => $row->name, + ]; + $form['table-row'][$row->id]['description'] = [ + '#type' => 'textfield', + '#required' => TRUE, + '#default_value' => $row->description, + ]; + // TableDrag: Weight column element. + $form['table-row'][$row->id]['weight'] = [ + '#type' => 'weight', + '#title' => $this->t('Weight for @title', ['@title' => $row->name]), + '#title_display' => 'invisible', + '#default_value' => $row->weight, + // Classify the weight element for #tabledrag. + '#attributes' => ['class' => ['table-sort-weight']], + ]; + } + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save All Changes'), + ]; + $form['actions']['cancel'] = [ + '#type' => 'submit', + '#value' => 'Cancel', + '#attributes' => [ + 'title' => $this->t('Return to TableDrag Overview'), + ], + '#submit' => ['::cancel'], + '#limit_validation_errors' => [], + ]; + + return $form; + } + + /** + * Form submission handler for the 'Return to' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function cancel(array &$form, FormStateInterface $form_state) { + $form_state->setRedirect('tabledrag_example.description'); + } + + /** + * Form submission handler for the simple form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Because the form elements were keyed with the item ids from the database, + // we can simply iterate through the submitted values. + $submission = $form_state->getValue('table-row'); + foreach ($submission as $id => $item) { + db_update('tabledrag_example') + ->fields([ + 'weight' => $item['weight'], + 'description' => $item['description'], + ]) + ->condition('id', $id, '=') + ->execute(); + } + } + +} diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.data.inc b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.data.inc new file mode 100644 index 000000000..0da5276d4 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.data.inc @@ -0,0 +1,77 @@ + t('Item One'), + 'description' => t('The first item'), + 'itemgroup' => t('Group1'), + ], + [ + 'name' => t('Item Two'), + 'description' => t('The second item'), + 'itemgroup' => t('Group1'), + ], + [ + 'name' => t('Item Three'), + 'description' => t('The third item'), + 'itemgroup' => t('Group1'), + ], + [ + 'name' => t('Item Four'), + 'description' => t('The fourth item'), + 'itemgroup' => t('Group2'), + ], + [ + 'name' => t('Item Five'), + 'description' => t('The fifth item'), + 'itemgroup' => t('Group2'), + ], + [ + 'name' => t('Item Six'), + 'description' => t('The sixth item'), + 'itemgroup' => t('Group2'), + ], + [ + 'name' => t('Item Seven'), + 'description' => t('The seventh item'), + 'itemgroup' => t('Group3'), + ], + [ + 'name' => t('Item Eight'), + 'description' => t('The eighth item'), + 'itemgroup' => t('Group3'), + ], + [ + 'name' => t('Item Nine'), + 'description' => t('The ninth item'), + 'itemgroup' => t('Group3'), + ], + [ + 'name' => t('Item Ten'), + 'description' => t('The tenth item'), + 'itemgroup' => t('Group4'), + ], + [ + 'name' => t('Item Eleven — A Root Node'), + 'description' => t('This item cannot be nested under a parent item'), + 'itemgroup' => t('Group4'), + ], + [ + 'name' => t('Item Twelve — A Leaf Item'), + 'description' => t('This item cannot have child items'), + 'itemgroup' => t('Group4'), + ], + ]; + return $rows; +} diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.info.yml b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.info.yml new file mode 100644 index 000000000..34247f3be --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.info.yml @@ -0,0 +1,15 @@ +name: Tabledrag Example +type: module +description: 'Demonstrates how to create tabledrag forms.' +package: Example modules +configure: tabledrag_example.description +# core: 8.x +dependencies: + - examples:examples + - drupal:node + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.install b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.install new file mode 100644 index 000000000..3c43957eb --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.install @@ -0,0 +1,101 @@ + 'Stores some entries for our tabledrag fun.', + 'fields' => [ + 'id' => [ + 'description' => 'The primary identifier for each item', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'name' => [ + 'description' => 'A name for this item', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ], + 'description' => [ + 'description' => 'A description for this item', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'itemgroup' => [ + 'description' => 'The group this item belongs to', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ], + 'weight' => [ + 'description' => 'The sortable weight for this item', + 'type' => 'int', + 'length' => 11, + 'not null' => TRUE, + 'default' => 0, + ], + 'pid' => [ + 'description' => 'The primary id of the parent for this item', + 'type' => 'int', + 'length' => 11, + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + ], + 'primary key' => ['id'], + ]; + return $schema; +} + +/** + * Implements hook_install(). + * + * This datafills the example items info which will be used in the example. + * + * @ingroup tabledrag_example + */ +function tabledrag_example_install() { + // Insert some values into the database. + $rows = tabledrag_example_data(); + if (db_table_exists('tabledrag_example')) { + foreach ($rows as $row) { + db_insert('tabledrag_example')->fields($row)->execute(); + } + } +} + +/** + * Implements hook_uninstall(). + * + * This removes the example data when the module is uninstalled. + * + * @ingroup tabledrag_example + */ +function tabledrag_example_uninstall() { + db_drop_table('tabledrag_example'); +} diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.links.menu.yml b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.links.menu.yml new file mode 100644 index 000000000..0c0947470 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.links.menu.yml @@ -0,0 +1,28 @@ +tabledrag_example.description: + title: Tabledrag Overview + route_name: tabledrag_example.description + expanded: TRUE + +tabledrag_example.simple_form: + title: Simple rows + route_name: tabledrag_example.simple_form + parent: tabledrag_example.description + weight: -4 + +tabledrag_example.parent_form: + title: Nested + route_name: tabledrag_example.parent_form + parent: tabledrag_example.description + weight: -3 + +tabledrag_example.rootleaf_form: + title: Roots and leaves + route_name: tabledrag_example.rootleaf_form + parent: tabledrag_example.description + weight: -2 + +tabledrag_example.reset_form: + title: Data reset + route_name: tabledrag_example.reset_form + parent: tabledrag_example.description + weight: -1 diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.module b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.module new file mode 100644 index 000000000..5b7157316 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.module @@ -0,0 +1,52 @@ +' . t("This module demonstrates how to code a module in order to use Drupal 8's TableDrag theme function.") . '

    '; + $help_text .= '

    ' . t('Reference links') . '

    '; + $help_text .= '
      '; + $help_text .= '
    • ' . $table_class_link->toString() . '
    • '; + $help_text .= '
    • ' . $table_change_link->toString() . '
    • '; + $help_text .= '
    • ' . $attach_change_link->toString() . '
    • '; + $help_text .= '
    '; + $help_text .= '

    ' . t('To see this Example module in action:') . ' ' . $example_link->toString() . '

    '; + return $help_text; + } +} + +/** + * @} End of "defgroup batch_example". + */ diff --git a/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.routing.yml b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.routing.yml new file mode 100644 index 000000000..d61f146d9 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/tabledrag_example.routing.yml @@ -0,0 +1,39 @@ +tabledrag_example.description: + path: 'examples/tabledrag-example' + defaults: + _controller: '\Drupal\tabledrag_example\Controller\TableDragExampleController::description' + _title: 'Tabledrag overview' + requirements: + _permission: 'access content' + +tabledrag_example.simple_form: + path: 'examples/tabledrag-example/row' + defaults: + _form: '\Drupal\tabledrag_example\Form\TableDragExampleSimpleForm' + _title: 'Simple rows tabledrag' + requirements: + _permission: 'access content' + +tabledrag_example.parent_form: + path: 'examples/tabledrag-example/nested' + defaults: + _form: '\Drupal\tabledrag_example\Form\TableDragExampleNestedForm' + _title: 'Nested tabledrag' + requirements: + _permission: 'access content' + +tabledrag_example.rootleaf_form: + path: 'examples/tabledrag-example/roots-and-leaves' + defaults: + _form: '\Drupal\tabledrag_example\Form\TableDragExampleRootLeafForm' + _title: 'Roots and leaves tabledrag' + requirements: + _permission: 'access content' + +tabledrag_example.reset_form: + path: 'examples/tabledrag-example/reset' + defaults: + _form: '\Drupal\tabledrag_example\Form\TableDragExampleResetForm' + _title: 'Tabledrag data reset' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/tabledrag_example/templates/description.html.twig b/sites/all/modules/examples/examples/tabledrag_example/templates/description.html.twig new file mode 100644 index 000000000..4a6f7bb4d --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/templates/description.html.twig @@ -0,0 +1,25 @@ +{# +/** + * @file + * Contains the text of the tabledrag_example explanation page + */ +#} + +{% set simple_rows = path('tabledrag_example.simple_form') %} +{% set nested = path('tabledrag_example.parent_form') %} +{% set roots_and_leaves = path('tabledrag_example.rootleaf_form') %} +{% set reset_form = path('tabledrag_example.reset_form') %} + +{% trans %} + +

    Below are examples of Drupal 8's table element with 'tabledrag' functionality.

    + +
      +
    1. Simple Rows
    2. +
    3. Nested
    4. +
    5. Roots and Leaves
    6. +
    + +

    Reset Tablesort Sample Data

    + +{% endtrans %} diff --git a/sites/all/modules/examples/examples/tabledrag_example/tests/src/Functional/TabledragMenuTest.php b/sites/all/modules/examples/examples/tabledrag_example/tests/src/Functional/TabledragMenuTest.php new file mode 100644 index 000000000..8d7faeff1 --- /dev/null +++ b/sites/all/modules/examples/examples/tabledrag_example/tests/src/Functional/TabledragMenuTest.php @@ -0,0 +1,87 @@ +drupalLogin( + $this->createUser(['access content']) + ); + + $assertion = $this->assertSession(); + + // Get the front page, which should only have the links in the sidebar. + $this->drupalGet(''); + foreach ($links as $path) { + $assertion->linkByHrefExists($path); + } + + // Get each path and verify a 200 response. + foreach ($links as $path) { + $this->drupalGet($path); + $assertion->statusCodeEquals(200); + } + + // Click all the submit and cancel buttons. + $pages = [ + 'tabledrag_example.simple_form' => ['Save All Changes', 'Cancel'], + 'tabledrag_example.parent_form' => ['Save All Changes', 'Cancel'], + 'tabledrag_example.rootleaf_form' => ['Save All Changes', 'Cancel'], + 'tabledrag_example.reset_form' => ['Yes, Reset It!'], + ]; + foreach ($pages as $route => $buttons) { + $path = Url::fromRoute($route); + foreach ($buttons as $button) { + $this->drupalPostForm($path, [], $button); + $assertion->statusCodeEquals(200); + } + } + // The reset form implements 'Cancel' as a link. + $this->drupalGet(Url::fromRoute('tabledrag_example.reset_form')); + $this->clickLink('Cancel'); + $assertion->statusCodeEquals(200); + } + +} diff --git a/sites/all/modules/examples/examples/tablesort_example/src/Controller/TableSortExampleController.php b/sites/all/modules/examples/examples/tablesort_example/src/Controller/TableSortExampleController.php new file mode 100644 index 000000000..7a600c296 --- /dev/null +++ b/sites/all/modules/examples/examples/tablesort_example/src/Controller/TableSortExampleController.php @@ -0,0 +1,87 @@ +get('database') + ); + } + + /** + * TableSortExampleController constructor. + * + * @param \Drupal\Core\Database\Connection $database + * The database connection. + */ + public function __construct(Connection $database) { + $this->database = $database; + } + + /** + * A simple controller method to explain what the tablesort example is about. + */ + public function description() { + // We are going to output the results in a table with a nice header. + $header = [ + // The header gives the table the information it needs in order to make + // the query calls for ordering. TableSort uses the field information + // to know what database column to sort by. + ['data' => t('Numbers'), 'field' => 't.numbers'], + ['data' => t('Letters'), 'field' => 't.alpha'], + ['data' => t('Mixture'), 'field' => 't.random'], + ]; + + // Using the TableSort Extender is what tells the query object that we + // are sorting. + $query = $this->database->select('tablesort_example', 't') + ->extend('Drupal\Core\Database\Query\TableSortExtender'); + $query->fields('t'); + + // Don't forget to tell the query object how to find the header information. + $result = $query + ->orderByHeader($header) + ->execute(); + + $rows = []; + foreach ($result as $row) { + // Normally we would add some nice formatting to our rows + // but for our purpose we are simply going to add our row + // to the array. + $rows[] = ['data' => (array) $row]; + } + + // Build the table for the nice output. + $build = [ + '#markup' => '

    ' . t('The layout here is a themed as a table + that is sortable by clicking the header name.') . '

    ', + ]; + $build['tablesort_table'] = [ + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + ]; + + return $build; + } + +} diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.info.yml b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.info.yml new file mode 100644 index 000000000..131da5f82 --- /dev/null +++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.info.yml @@ -0,0 +1,14 @@ +name: Table Sort Example +type: module +description: 'Demonstrates how to create sortable output in a table.' +package: Example modules +# core: 8.x +dependencies: + - drupal:node + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.install b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.install new file mode 100644 index 000000000..ac909f89a --- /dev/null +++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.install @@ -0,0 +1,80 @@ + 1, 'alpha' => 'e', 'random' => '912cv21'], + ['numbers' => 2, 'alpha' => 'a', 'random' => '0kuykuh'], + ['numbers' => 3, 'alpha' => 'm', 'random' => 'fuye8734h'], + ['numbers' => 4, 'alpha' => 'w', 'random' => '80jsv772'], + ['numbers' => 5, 'alpha' => 'o', 'random' => 'd82sf-csj'], + ['numbers' => 6, 'alpha' => 's', 'random' => 'au832'], + ['numbers' => 7, 'alpha' => 'e', 'random' => 't982hkv'], + ]; + + $db_connection = \Drupal::database(); + if ($db_connection->schema()->tableExists('tablesort_example')) { + foreach ($rows as $row) { + db_insert('tablesort_example')->fields($row)->execute(); + } + } +} + +/** + * Implements hook_uninstall(). + * + * It's good to clean up after ourselves. + * + * @ingroup tablesort_example + */ +function tablesort_example_uninstall() { + $db_connection = \Drupal::database(); + $db_connection->schema()->dropTable('tablesort_example'); +} + +/** + * Implements hook_schema(). + * + * @ingroup tablesort_example + */ +function tablesort_example_schema() { + $schema['tablesort_example'] = [ + 'description' => 'Stores some values for sorting fun.', + 'fields' => [ + 'numbers' => [ + 'description' => 'This column simply holds numbers values', + 'type' => 'varchar', + 'length' => 2, + 'not null' => TRUE, + ], + 'alpha' => [ + 'description' => 'This column simply holds alpha values', + 'type' => 'varchar', + 'length' => 2, + 'not null' => TRUE, + ], + 'random' => [ + 'description' => 'This column simply holds random values', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + ], + ], + 'primary key' => ['numbers'], + ]; + + return $schema; +} diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.links.menu.yml b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.links.menu.yml new file mode 100644 index 000000000..080060793 --- /dev/null +++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.links.menu.yml @@ -0,0 +1,3 @@ +tablesort_example.description: + title: TableSort Example + route_name: tablesort_example_description diff --git a/sites/all/modules/examples/examples/tablesort_example/tablesort_example.module b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.module new file mode 100644 index 000000000..c3b465849 --- /dev/null +++ b/sites/all/modules/examples/examples/tablesort_example/tablesort_example.module @@ -0,0 +1,20 @@ +assertSession(); + + // No need to login for this test. + $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'desc', 'order' => 'Numbers']]); + $assert->statusCodeEquals(200); + // Ordered by number decending. + $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[1]'); + $this->assertEquals(7, $item->getText(), 'Ordered by number decending.'); + + $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'asc', 'order' => 'Numbers']]); + $assert->statusCodeEquals(200); + // Ordered by Number ascending. + $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[1]'); + $this->assertEquals(1, $item->getText(), 'Ordered by Number ascending.'); + + // Sort by Letters. + $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'desc', 'order' => 'Letters']]); + $assert->statusCodeEquals(200); + // Ordered by Letters decending. + $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[2]'); + $this->assertEquals('w', $item->getText(), 'Ordered by Letters decending.'); + + $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'asc', 'order' => 'Letters']]); + $assert->statusCodeEquals(200); + // Ordered by Letters ascending. + $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[2]'); + $this->assertEquals('a', $item->getText(), 'Ordered by Letters ascending.'); + + // Sort by Mixture. + $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'desc', 'order' => 'Mixture']]); + $assert->statusCodeEquals(200); + // Ordered by Mixture decending. + $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[3]'); + $this->assertEquals('t982hkv', $item->getText(), 'Ordered by Mixture decending.'); + + $this->drupalGet('/examples/tablesort-example', ['query' => ['sort' => 'asc', 'order' => 'Mixture']]); + $assert->statusCodeEquals(200); + // Ordered by Mixture ascending. + $item = $this->getSession()->getPage()->find('xpath', '//tbody/tr/td[3]'); + $this->assertEquals('0kuykuh', $item->getText(), 'Ordered by Mixture ascending.'); + + } + + /** + * Data provider for testing menu links. + * + * @return array + * Array of page -> link relationships to check for. + * The key is the path to the page where our link should appear. + * The value is the link that should appear on that page. + */ + protected function providerMenuLinks() { + return [ + '' => '/examples/tablesort-example', + ]; + } + + /** + * Verify and validate that default menu links were loaded for this module. + */ + public function testTableSortExampleLink() { + $assert = $this->assertSession(); + + $links = $this->providerMenuLinks(); + foreach ($links as $page => $path) { + $this->drupalGet($page); + $assert->linkByHrefExists($path); + } + } + + /** + * Tests tablesort_example menus. + */ + public function testTableSortExampleMenu() { + $assert = $this->assertSession(); + + $this->drupalGet('/examples/tablesort-example'); + $assert->statusCodeEquals(200); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/src/Controller/ContrivedController.php b/sites/all/modules/examples/examples/testing_example/src/Controller/ContrivedController.php new file mode 100644 index 000000000..6538f88cf --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/src/Controller/ContrivedController.php @@ -0,0 +1,93 @@ +get('string_translation') + ); + } + + /** + * Construct a new controller. + * + * @param Drupal\Core\StringTranslation\TranslationInterface $translation + * The translation service. + */ + public function __construct(TranslationInterface $translation) { + $this->setStringTranslation($translation); + } + + /** + * A controller method which displays a sum in terms of hands. + * + * @param int $first + * A parameter to the controller path. + * @param int $second + * A parameter to the controller path. + * + * @return string[] + * A markup array. + */ + public function displayAddedNumbers($first, $second) { + return [ + '#markup' => '

    ' . $this->handCount($first, $second) . '

    ', + ]; + } + + /** + * Generate a message based on how many hands are needed to count the sum. + * + * @param int $first + * First parameter. + * @param int $second + * Second parameter. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The translated message. + */ + protected function handCount($first, $second) { + $sum = abs($this->add((int) $first, (int) $second)); + if ($sum <= 5) { + $message = $this->t('I can count these on one hand.'); + } + elseif ($sum <= 10) { + $message = $this->t('I need two hands to count these.'); + } + else { + $message = $this->t("That's just too many numbers to count."); + } + return $message; + } + + /** + * Add two numbers. + * + * @param int $first + * The first parameter. + * @param int $second + * The second parameter. + * + * @return int + * The sum of the two parameters. + */ + protected function add($first, $second) { + return $first + $second; + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/src/Controller/TestingExampleController.php b/sites/all/modules/examples/examples/testing_example/src/Controller/TestingExampleController.php new file mode 100644 index 000000000..b7fa02ab6 --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/src/Controller/TestingExampleController.php @@ -0,0 +1,26 @@ +assertTrue(TRUE); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/templates/description.html.twig b/sites/all/modules/examples/examples/testing_example/templates/description.html.twig new file mode 100644 index 000000000..378291464 --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/templates/description.html.twig @@ -0,0 +1,79 @@ +{# + +Description text for the PHPUnit Example. + +#} + +{% trans %} +

    Testing Frameworks in Drupal

    + +

    How to use this example module

    +

    You really should be reading the various docblocks in the files under + tests/src/.

    + +

    How To:

    +
      +
    • PHPUnit-based Drupal tests go in the tests/src directory, so + they will not be loaded by the autoloader during normal bootstrap. +
    • + +
    • Unit tests go in [your_module]/tests/src/Unit.
    • +
    • Kernel tests go in [your_module]/tests/src/Kernel.
    • +
    • Functional tests go in [your_module]/tests/src/Functional.
    • +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TypeLocationNamespaceSubclass of
    Unit test[your_module]/tests/src/UnitDrupal\Tests\[your_module]\Unit\Drupal\Tests\UnitTestCase
    Kernel test[your_module]/tests/src/KernelDrupal\Tests\[your_module]\Kernel\Drupal\KernelTests\KernelTestBase
    Functional test[your_module]/tests/src/FunctionalDrupal\Tests\[your_module]\Functional\Drupal\Tests\BrowserTestBase
    FunctionalJavascript test[your_module]/tests/src/FunctionalJavascriptDrupal\Tests\[your_module]\FunctionalJavascript\Drupal\Tests\JavascriptTestBase
    Shared test traits[your_module]/tests/src/TraitsDrupal\Tests\[your_module]\Traits\n/a
    + +

    Standard PHPUnit Practices

    +

    You can run PHPUnit from the command line or via the run-tests.sh command.

    +

    You can specify which type of test you're running via +

    phpunit --testsuite
    and +
    run-tests.sh --types
    .
    +

    There are many options to phpunit, but, for instance, to run all of the + testing_example phpunit tests, you could +

    ./vendor/phpunit/phpunit/phpunit -c core/ ./modules/examples/testing_example
    +

    + +{% endtrans %} diff --git a/sites/all/modules/examples/examples/testing_example/testing_example.info.yml b/sites/all/modules/examples/examples/testing_example/testing_example.info.yml new file mode 100644 index 000000000..457ba16fb --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/testing_example.info.yml @@ -0,0 +1,13 @@ +name: Testing Example +type: module +description: Demonstrates Drupal core testing frameworks +package: Example modules +# core: 8.x +dependencies: + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/testing_example/testing_example.links.menu.yml b/sites/all/modules/examples/examples/testing_example/testing_example.links.menu.yml new file mode 100644 index 000000000..f5b775a3e --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/testing_example.links.menu.yml @@ -0,0 +1,8 @@ +testing_example.description: + title: Testing Example + route_name: testing_example.description + expanded: TRUE +testing_example.sum_in_hands: + title: Sum in hands + route_name: testing_example.sum_in_hands + parent: testing_example.description diff --git a/sites/all/modules/examples/examples/testing_example/testing_example.module b/sites/all/modules/examples/examples/testing_example/testing_example.module new file mode 100644 index 000000000..e44d99b8f --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/testing_example.module @@ -0,0 +1,17 @@ +adminUser = $this->drupalCreateUser([ + 'access administration pages', + 'view the administration theme', + 'administer permissions', + 'administer nodes', + 'administer content types', + ]); + $this->authUser = $this->drupalCreateUser([], 'authuser'); + + // We have to create a content type because testing uses the 'testing' + // profile, which has no content types by default. + // Although we could have visited admin pages and pushed buttons to create + // the content type, there happens to be function we can use in this case. + $this->createContentType(['type' => 'test_content_type']); + } + + /** + * Demonstrate node creation through UI interaction. + */ + public function testNewPage() { + // We log in an administrator because they will have permissions to create + // content. + $this->drupalLogin($this->adminUser); + + // For many assertions, we need a WebAssert object. This object gives us + // assertion types for the HTTP requests we make, such as content and the + // HTTP status code. + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + + // Get the page that lets us add new content. + $this->drupalGet('node/add/test_content_type'); + // Use the WebAssert object to assert the HTTP status code. + $assert->statusCodeEquals(200); + + // Set up our new piece of content. + $nodeTitle = 'Test node for testNewPage'; + $edit = [ + 'title[0][value]' => $nodeTitle, + 'body[0][value]' => 'Body of test node', + ]; + // Tell Drupal to post our new content. We post to NULL for the URL which + // tells drupalPostForm() to use the current page. + $this->drupalPostForm(NULL, $edit, 'op'); + // Check our expectations. + $assert->statusCodeEquals(200); + $assert->linkExists($nodeTitle); + + // Log in our non-admin user and navigate to the node. + $this->drupalLogin($this->authUser); + + // We can search for the node by its title. Since the node object can also + // tell us its URL, we can just feed that information into drupalGet(). + /** @var \Drupal\node\NodeInterface $createdNode */ + $createdNode = $this->drupalGetNodeByTitle($nodeTitle); + $url = $createdNode->toUrl(); + $this->drupalGet($url); + $assert->statusCodeEquals(200); + + // Look at the page title. + $assert->titleEquals("{$nodeTitle} | Drupal"); + + // Find the title of the node itself. + $nodeTitleElement = $this->getSession() + ->getPage() + ->find('css', 'h1 span.field--name-title'); + $this->assertEquals($nodeTitleElement->getText(), $nodeTitle); + } + + /** + * Demonstrate node creation via NodeCreationTrait::createNode. + */ + public function testNewPageApiCreate() { + $assert = $this->assertSession(); + + $this->drupalLogin($this->adminUser); + + $nodeTitle = 'Test node for testNewPageApiCreate'; + + // Create new node using API. + $node = $this->drupalCreateNode([ + 'type' => 'test_content_type', + 'title' => $nodeTitle, + 'body' => [ + [ + 'format' => filter_default_format($this->adminUser), + 'value' => 'Body of test node', + ], + ], + ]); + $node->save(); + $url = $node->toUrl(); + + // Confirm page creation. + $this->drupalGet($url); + $assert->statusCodeEquals(200); + + // Log in our normal user and navigate to the node. + $this->drupalLogin($this->authUser); + $this->drupalGet($url); + $assert->statusCodeEquals(200); + + // Look at the *page* title. + $assert->titleEquals("{$nodeTitle} | Drupal"); + + // Find the title of the node itself. + $nodeTitleElement = $this->getSession() + ->getPage() + ->find('css', 'h1 span.field--name-title'); + $this->assertEquals($nodeTitleElement->getText(), $nodeTitle); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkDependenciesTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkDependenciesTest.php new file mode 100644 index 000000000..178804860 --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkDependenciesTest.php @@ -0,0 +1,69 @@ +placeBlock('system_menu_block:tools'); + // Add a content type. + $this->contentType = $this->createContentType(); + } + + /** + * Tests for the existence of a default menu item on the home page. + * + * We'll open the home page and look for the Tools menu link called 'Add + * content.' + */ + public function testAddContentMenuItem() { + // Step 1: Log in a user who can add content. + $this->drupalLogin( + $this->createUser([ + 'create ' . $this->contentType->id() . ' content', + ]) + ); + + // Step 2: Visit the home path. + $this->drupalGet($this->buildUrl('')); + // Step 3: Look on the page for the 'Add content' link. + $this->assertSession()->linkExists('Add content'); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkTest.php new file mode 100644 index 000000000..9dedaf6c6 --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/FrontPageLinkTest.php @@ -0,0 +1,43 @@ +drupalLogin( + $this->createUser([ + 'create article content', + ]) + ); + + // Step 2: Visit the home path. + $this->drupalGet($this->buildUrl('')); + // Step 3: Look on the page for the 'Add content' link. + $this->assertSession()->linkExists('Add content'); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/SkeletonTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/SkeletonTest.php new file mode 100644 index 000000000..1f997a01e --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/SkeletonTest.php @@ -0,0 +1,80 @@ +fail('The test runner found our test and failed it. Yay!'); + // + // PHPUnit will complain if there is no assertion in our test method, so we + // add this passing assertion. Comment or delete it if you'd like. + $this->assertTrue(TRUE); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Functional/TestingExampleMenuTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/TestingExampleMenuTest.php new file mode 100644 index 000000000..35bfb8fbb --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/tests/src/Functional/TestingExampleMenuTest.php @@ -0,0 +1,49 @@ + '/examples/testing-example'] as $page => $path) { + $this->drupalGet($page); + $this->assertLinkByHref($path); + } + $this->drupalGet('/examples/testing-example'); + $this->assertResponse(200, 'Description page exists.'); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Kernel/ExampleFixtureManagementTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Kernel/ExampleFixtureManagementTest.php new file mode 100644 index 000000000..d3cdddbfa --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/tests/src/Kernel/ExampleFixtureManagementTest.php @@ -0,0 +1,121 @@ +installSchema('system', ['sequences']); + + // Install *module* schema for node/user modules. + $this->installSchema('node', ['node_access']); + $this->installSchema('user', ['users_data']); + + // Install *entity* schema for the node entity. + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + + // Install any config provided by the enabled. + $this->installConfig(['field', 'node', 'text', 'filter', 'user']); + + // Finally, create an 'owner' account. + $this->owner = $this->createUser([], 'testuser'); + } + + /** + * Create a node by using createNode() from NodeCreationTrait. + */ + public function testNodeCreation() { + // Unless there's a specific reason to do so, strings in tests should not be + // translated with t(). + $nodeTitle = 'Test Node!'; + + /** @var \Drupal\node\NodeInterface $node */ + $node = $this->createNode([ + 'title' => $nodeTitle, + 'type' => 'page', + 'uid' => $this->owner->id(), + ]); + + // Assert that the node we created has the title we expect. + $this->assertEquals($nodeTitle, $node->getTitle()); + } + + /** + * Create a user account using createUser() from the UserCreation trait. + */ + public function testUserCreation() { + // Create a user named 'extrauser'. + $account = $this->createUser([], 'extrauser'); + // Assert that this user exists. + $this->assertEquals('extrauser', $account->getAccountName()); + + // Assert that our auth user is not the same user as extrauser. + $this->assertNotEquals($this->owner->getAccountName(), $account->getAccountName()); + } + +} diff --git a/sites/all/modules/examples/examples/testing_example/tests/src/Unit/Controller/ContrivedControllerTest.php b/sites/all/modules/examples/examples/testing_example/tests/src/Unit/Controller/ContrivedControllerTest.php new file mode 100644 index 000000000..0d060b402 --- /dev/null +++ b/sites/all/modules/examples/examples/testing_example/tests/src/Unit/Controller/ContrivedControllerTest.php @@ -0,0 +1,118 @@ +getMockBuilder(ContrivedController::class) + ->disableOriginalConstructor() + ->getMock(); + $ref_add = new \ReflectionMethod($controller, 'add'); + $ref_add->setAccessible(TRUE); + $this->assertEquals($expected, $ref_add->invokeArgs($controller, [$first, $second])); + } + + /** + * Data provider for testHandCount(). + */ + public function provideTestHandCount() { + return [ + ['I can count these on one hand.', 0, 0], + ['I can count these on one hand.', 1, 0], + ['I can count these on one hand.', 0, 1], + ['I need two hands to count these.', 5, 5], + ['That\'s just too many numbers to count.', 5, 6], + ['That\'s just too many numbers to count.', 6, 5], + ]; + } + + /** + * @dataProvider provideTestHandCount + */ + public function testHandCount($expected, $first, $second) { + // Get a mock translation service. + $mock_translation = $this->getStringTranslationStub(); + // Create a new controller with our mocked translation service. + $controller = new ContrivedController($mock_translation); + + // Set up a reflection for handCount(). + $ref_hand_count = new \ReflectionMethod($controller, 'handCount'); + // Set handCount() to be public. + $ref_hand_count->setAccessible(TRUE); + // Check out whether handCount() meets our expectation. + $message = $ref_hand_count->invokeArgs($controller, [$first, $second]); + $this->assertEquals($expected, (string) $message); + } + + /** + * Data provider for testHandCountIsolated(). + */ + public function providerTestHandCountIsolated() { + $data = []; + + // Add one-hand data. + foreach (range(0, 5) as $sum) { + $data[] = ['I can count these on one hand.', $sum]; + } + + // Add two-hand data. + foreach (range(6, 10) as $sum) { + $data[] = ['I need two hands to count these.', $sum]; + } + + // Add too-many data. + foreach (range(11, 15) as $sum) { + $data[] = ['That\'s just too many numbers to count.', $sum]; + } + + return $data; + } + + /** + * @dataProvider providerTestHandCountIsolated + */ + public function testHandCountIsolated($expected, $sum) { + // Mock a ContrivedController, using a mocked translation service. + $controller = $this->getMockBuilder(ContrivedController::class) + ->setConstructorArgs([$this->getStringTranslationStub()]) + // Specify that we'll also mock add(). + ->setMethods(['add']) + ->getMock(); + + // Mock add() so that it returns our $sum when it's called with (0,0). + $controller->expects($this->once()) + ->method('add') + ->with($this->equalTo(0), $this->equalTo(0)) + ->willReturn($sum); + + // Use reflection to make handCount() public. + $ref_hand_count = new \ReflectionMethod($controller, 'handCount'); + $ref_hand_count->setAccessible(TRUE); + + // Invoke handCount(). + $message = (string) $ref_hand_count->invokeArgs($controller, [0, 0]); + + // Assert our expectations. + $this->assertEquals($expected, $message); + } + +} diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.info.yml b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.info.yml new file mode 100644 index 000000000..0067bbb45 --- /dev/null +++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.info.yml @@ -0,0 +1,13 @@ +name: 'Sample Description Template Test' +type: module +description: 'Support module for testing the DescriptionTemplateTrait.' +package: Testing +# core: 8.x +dependencies: + - examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.routing.yml b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.routing.yml new file mode 100644 index 000000000..685d52820 --- /dev/null +++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/examples_description_test.routing.yml @@ -0,0 +1,6 @@ +example_description_trait_test.description: + path: 'examples/tests/example-description' + defaults: + _controller: '\Drupal\examples_description_test\Controller\SampleExampleController::description' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/src/Controller/SampleExampleController.php b/sites/all/modules/examples/examples/tests/modules/examples_description_test/src/Controller/SampleExampleController.php new file mode 100644 index 000000000..218e45f8b --- /dev/null +++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/src/Controller/SampleExampleController.php @@ -0,0 +1,34 @@ + $this->getModuleName(), + 'slogan' => $this->t('We aim to please'), + ]; + return $variables; + } + +} diff --git a/sites/all/modules/examples/examples/tests/modules/examples_description_test/templates/description.html.twig b/sites/all/modules/examples/examples/tests/modules/examples_description_test/templates/description.html.twig new file mode 100644 index 000000000..3e8d3971a --- /dev/null +++ b/sites/all/modules/examples/examples/tests/modules/examples_description_test/templates/description.html.twig @@ -0,0 +1,25 @@ +{* + +Description template test example. + +This is a test template that demonstrates how an Examples module can +implement its description controller by using a Twig template. + +*} + + + +{% trans %} + +

    Sample Description

    + +

    Here is a sample description. It embeds some Twig variables.

    + +
      +
    1. Used in module: {{ module }}.
    2. +
    3. Our slogan for today: {{slogan}}.
    4. +
    + +{% endtrans %} diff --git a/sites/all/modules/examples/examples/tests/src/Functional/ExamplesBrowserTestBase.php b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesBrowserTestBase.php new file mode 100644 index 000000000..70c599d51 --- /dev/null +++ b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesBrowserTestBase.php @@ -0,0 +1,47 @@ +setupExamplesMenus(); + } + + /** + * Set up menus and tasks in their regions. + * + * Since menus and tasks are now blocks, we're required to explicitly set them + * to regions. This method standardizes the way we do that for Examples. + * + * Note that subclasses must explicitly declare that the block module is a + * dependency. + */ + protected function setupExamplesMenus() { + $this->drupalPlaceBlock('system_menu_block:tools', ['region' => 'primary_menu']); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'secondary_menu']); + $this->drupalPlaceBlock('local_actions_block', ['region' => 'content']); + $this->drupalPlaceBlock('page_title_block', ['region' => 'content']); + } + +} diff --git a/sites/all/modules/examples/examples/tests/src/Functional/ExamplesTest.php b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesTest.php new file mode 100644 index 000000000..c503a715d --- /dev/null +++ b/sites/all/modules/examples/examples/tests/src/Functional/ExamplesTest.php @@ -0,0 +1,74 @@ +assertSession(); + + // Verify that the toolbar tab and tray are showing and functioning. + $user = $this->drupalCreateUser(['access toolbar']); + $this->drupalLogin($user); + + // Check for the 'Examples' tab. + $this->drupalGet(''); + $assert->statusCodeEquals(200); + + // Assert that the toolbar tab registered by examples is present. + $assert->linkExists('Examples'); + + // Assert that the toolbar tab registered by examples is present. + $this->assertEquals( + 1, + \count($this->xpath('//nav/div/a[@data-toolbar-tray="toolbar-item-examples-tray"]')), + 'Found the Examples toolbar tab.' + ); + + // Assert that the toolbar tray registered by examples is present. + $this->assertEquals( + 1, + \count($this->xpath('//nav/div/div[@data-toolbar-tray="toolbar-item-examples-tray"]')), + 'Found the Examples toolbar tray.' + ); + // Assert that PHPUnit link does not appears in the tray. + $phpunit_link = 'PHPUnit Example'; + $assert->linkNotExists($phpunit_link); + $assert->pageTextNotContains('
  • '); + + // Install phpunit_example and see if it appears in the toolbar. We use + // phpunit_example because it's very light-weight. + $this->container->get('module_installer')->install(['phpunit_example'], TRUE); + // SimpleTest needs for us to reset all the caches. + $this->resetAll(); + + // Verify that PHPUnit appears in the tray. + $this->drupalGet(''); + $assert->linkExists($phpunit_link); + // Assert that the PHPUnit tray item is present. + $this->assertEquals( + 1, + \count($this->xpath('//nav/div/div/nav/ul/li[@class="phpunit-example"]')), + 'Found the PHPUnit Example tray item.' + ); + + } + +} diff --git a/sites/all/modules/examples/examples/tests/src/Kernel/DescriptionTraitTest.php b/sites/all/modules/examples/examples/tests/src/Kernel/DescriptionTraitTest.php new file mode 100644 index 000000000..817fcb7de --- /dev/null +++ b/sites/all/modules/examples/examples/tests/src/Kernel/DescriptionTraitTest.php @@ -0,0 +1,41 @@ +container); + // We want to test ::getDescriptionTemplatePath(), which is a protected + // method. Use a little of the Old Black Reflection Magic. + $ref_get_path = new \ReflectionMethod($sample_controller, 'getDescriptionTemplatePath'); + $ref_get_path->setAccessible(TRUE); + $this->assertFileExists($ref_get_path->invoke($sample_controller)); + // And get our render output. + $render_array = $sample_controller->description(); + // We cast to string, since renderPlain() returns a markup object. + $output = (string) $this->container->get('renderer')->renderPlain($render_array); + // Did the template load? + $this->assertContains('Template loaded!', $output); + // Were the variables resolved correctly? + $this->assertContains('Used in module: examples_description_test.', $output); + $this->assertContains('Our slogan for today: We aim to please.', $output); + } + +} diff --git a/sites/all/modules/examples/examples/tests/src/Unit/YamlValidationTest.php b/sites/all/modules/examples/examples/tests/src/Unit/YamlValidationTest.php new file mode 100644 index 000000000..1f6b5c532 --- /dev/null +++ b/sites/all/modules/examples/examples/tests/src/Unit/YamlValidationTest.php @@ -0,0 +1,49 @@ +getPathname(); + if (strpos($pathname, '.yml') !== FALSE) { + if (strpos($pathname, '/config/') !== FALSE) { + $yaml_paths[] = [$pathname]; + } + } + } + return $yaml_paths; + } + + /** + * @dataProvider provideYamls + */ + public function testNoUuidsInConfig($yaml_path) { + $yaml = Yaml::parse(file_get_contents($yaml_path)); + $this->assertArrayNotHasKey('uuid', $yaml, "YAML in this file contains a uuid key: $yaml_path"); + } + +} diff --git a/sites/all/modules/examples/examples/tour_example/config/install/tour.tour.tour-example.yml b/sites/all/modules/examples/examples/tour_example/config/install/tour.tour.tour-example.yml new file mode 100644 index 000000000..b795e1b96 --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/config/install/tour.tour.tour-example.yml @@ -0,0 +1,103 @@ +# This file defines the tour for our example page. There should be one tour +# file for each tour that you create. These tour files should be placed in a +# module's 'config' folder and named using the pattern +# 'tour.tour.{tour-id}.yml'. +# +# Each tour file has two parts: 1) The tour properties, and 2) the tip +# definitions. +# +# +# TOUR PROPERTIES +# +# The tour properties define information that applies to the tour as a whole. +# Properties that you can define for your tours include the following: +# +# id: Each tour should have a unique ID. This id is used in the +# filename and appears as an ID in the HTML. +# module: The machine name of the module containing your tour. +# label: A human readable name for the tour. +# status: A boolean, defining whether the tour is enabled. +# langcode: A two-letter language code defining the language of your tour. +# routes: An array of routes for which the tour is active. Specify these +# as an array with route_name and optional route_params (also an +# array). Route names are found in each module's routing.yml file. +# +# +# TIP DEFINITIONS +# +# Here, you define each tip that you want to appear in your tour. Each of +# the tips have properties that must be defined, including the following: +# +# id: Each tip needs a unique ID. This appears as an ID in the HTML. +# plugin: The Tour API uses plugins for defining different types of +# tips. The 'text' plugin (for making text tooltips) is provided +# in core, but developers can define additional plugins for tips +# containing images, video, or other interactions. +# label: The lable for the tip. This will be rendered in an h3 element. +# body: The body of the tip. HTML markup is allowed. +# weight: Tips within a tour are ordered by weight, beginning with the +# lowest number. Negative values are acceptable. +# location: Defines the location of the tip, relative to its target. +# Acceptable values include: top, bottom, left, & right. If the +# location is not defined, the default value (bottom) will be +# used. +# attributes: Attributes provdied to the tip for various purposes. Use the +# following attributes to control the placement of the tip: +# +# data-id: Places this tip on the DOM element containing +# this ID. +# data-class: Places this tip on the DOM element containing +# this class. +# +# If you omit both the .data-id and .data-class, the tip will be +# shown as modal instead of being targeted to an element. +# +id: tour-example +module: tour_example +label: 'Tour an example admin page' +langcode: en +routes: + - route_name: tour_example_description +tips: + introduction: + id: introduction + plugin: text + label: 'Introduction' + body: 'This is an example tour. Click "next" to continue through the tour.' + weight: 1 + first-item: + id: first-item + plugin: text + label: 'First Item' + body: 'Tours are helpful for walking users through an unfamiliar admin interface.' + weight: 2 + location: bottom + attributes: + data-id: tour-id-1 + second-item: + id: second-item + plugin: text + label: 'Second Item' + body: 'Individual tips can be positioned anywhere on the page.' + weight: 3 + location: top + attributes: + data-id: tour-id-2 + third-item: + id: third-item + plugin: text + label: 'Third Item' + body: 'You can click the X in the top right corner of this box to close the tour at any time.' + weight: 4 + location: bottom + attributes: + data-id: tour-id-3 + fourth-item: + id: fourth-item + plugin: text + label: 'Fourth Item' + body: 'When the tour is over, click "End Tour".' + weight: 5 + location: left + attributes: + data-id: tour-id-4 diff --git a/sites/all/modules/examples/examples/tour_example/src/Controller/TourExampleController.php b/sites/all/modules/examples/examples/tour_example/src/Controller/TourExampleController.php new file mode 100644 index 000000000..7eb50ed60 --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/src/Controller/TourExampleController.php @@ -0,0 +1,24 @@ +The Tour: +

    Click the 'Tour' icon in the admin menu bar to start.

    +
    First item.
    +
    Second item.
    +
    Third item.
    +
    Fourth item.
    + +

    About Tours

    + +

    The Tour module allows you you make instructional tours of user interfaces.

    + +

    The Tour module comes with Drupal 8 and makes it easy for developers to add + "Tours" for guiding users through unfamiliar user interfaces.

    + +

    Each tour is comprised of a series of tooltips that provide contextual + information about an interface. The user can start a tour by clicking the + "Tour" icon on the right side of the Drupal 8 toolbar. The tour icon is only + visible when there is a tour available on the current page.

    + +

    The Tour module provides the Tour API, which makes it easy for developers to + add tours to their modules. In most cases, adding a tour is as simple as + creating a YAML file in the config directory in their module, containing the + expected data. For a detailed example of such a file, see + config/install/tour.tour.tour-example.yml.

    + +

    If you are interested in building tours through a user interface, you may + want to look at the Tour UI module: https://drupal.org/project/tour_ui

    + +

    The Tour module uses the Joyride jQuery plugin for its underlying + functionality. You can find more information about Joyride at + https://github.com/zurb/joyride

    + +{% endtrans %} diff --git a/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourExampleTest.php b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourExampleTest.php new file mode 100644 index 000000000..672406be4 --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourExampleTest.php @@ -0,0 +1,54 @@ +assertSession(); + + // Test for a link to the tour_example in the Tools menu. + $this->drupalGet(''); + $assert->statusCodeEquals(200); + $assert->linkByHrefExists('examples/tour-example'); + + // Verify if the can successfully access the tour_examples page. + $this->drupalGet('examples/tour-example'); + $assert->statusCodeEquals(200); + + // Verify that the tour tips exist on this page. + $this->assertTourTips(); + } + +} diff --git a/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBase.php b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBase.php new file mode 100644 index 000000000..d9c703f0e --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBase.php @@ -0,0 +1,82 @@ +assertTourTips(); + * + * // Advanced example. The following would be used for multipage or + * // targeting a specific subset of tips. + * $tips = array(); + * $tips[] = array('data-id' => 'foo'); + * $tips[] = array('data-id' => 'bar'); + * $tips[] = array('data-class' => 'baz'); + * $this->assertTourTips($tips); + * @endcode + */ + public function assertTourTips(array $tips = []) { + $assert = $this->assertSession(); + // Get the rendered tips and their data-id and data-class attributes. + if (empty($tips)) { + // Tips are rendered as
  • elements inside
      . + $rendered_tips = $this->xpath('//ol[@id = "tour"]//li[starts-with(@class, "tip")]'); + foreach ($rendered_tips as $rendered_tip) { + $item = []; + if ($rendered_tip->hasAttribute('data-id')) { + $item['data-id'] = $rendered_tip->getAttribute('data-id'); + } + if ($rendered_tip->hasAttribute('data-class')) { + $item['data-class'] = $rendered_tip->getAttribute('data-class'); + } + if (!empty($item)) { + $tips[] = $item; + } + } + } + + // If the tips are still empty we need to fail. + if (empty($tips)) { + $this->fail('Could not find tour tips on the current page.'); + } + else { + // Check for corresponding page elements. + $total = 0; + $modals = 0; + foreach ($tips as $tip) { + if (!empty($tip['data-id'])) { + $elements = $this->getSession()->getPage()->find('css', "#{$tip['data-id']}"); + $this->assertTrue(!empty($elements) && count($elements) === 1, format_string('Found corresponding page element for tour tip with id #%data-id', ['%data-id' => $tip['data-id']])); + } + elseif (!empty($tip['data-class'])) { + $elements = $this->getSession()->getPage()->find('css', "#{$tip['data-class']}"); + + $this->assertFalse(empty($elements), format_string('Found corresponding page element for tour tip with class .%data-class', ['%data-class' => $tip['data-class']])); + } + else { + // It's a modal. + $modals++; + } + $total++; + } + $this->verbose(format_string('Total %total Tips tested of which %modals modal(s).', ['%total' => $total, '%modals' => $modals])); + } + } + +} diff --git a/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBasic.php b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBasic.php new file mode 100644 index 000000000..a2063780d --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/tests/src/Functional/TourTestBasic.php @@ -0,0 +1,76 @@ + array( + * array('data-id' => 'foo'), + * array('data-class' => 'bar'), + * ), + * ); + * @endcode + */ + protected $tips = []; + + /** + * An admin user with administrative permissions for tour. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * The permissions required for a logged in user to test tour tips. + * + * @var array + * A list of permissions. + */ + protected $permissions = ['access tour']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Make sure we are using distinct default and administrative themes for + // the duration of these tests. + $this->container->get('theme_handler')->install(['bartik', 'seven']); + $this->config('system.theme') + ->set('default', 'bartik') + ->set('admin', 'seven') + ->save(); + + $this->permissions[] = 'view the administration theme'; + + // Create an admin user to view tour tips. + $this->adminUser = $this->drupalCreateUser($this->permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * A simple tip test. + */ + public function testTips() { + foreach ($this->tips as $path => $attributes) { + $this->drupalGet($path); + $this->assertTourTips($attributes); + } + } + +} diff --git a/sites/all/modules/examples/examples/tour_example/tour_example.info.yml b/sites/all/modules/examples/examples/tour_example/tour_example.info.yml new file mode 100644 index 000000000..fa44f204a --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/tour_example.info.yml @@ -0,0 +1,15 @@ +name: 'Tour Example' +type: module +description: 'Demonstrates how to create a tour in Drupal 8.' +package: Example modules +# core: 8.x +dependencies: + - drupal:tour + - drupal:toolbar + - examples:examples + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.x-dev' +core: '8.x' +project: 'examples' +datestamp: 1513537386 diff --git a/sites/all/modules/examples/examples/tour_example/tour_example.links.menu.yml b/sites/all/modules/examples/examples/tour_example/tour_example.links.menu.yml new file mode 100644 index 000000000..70d574dc1 --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/tour_example.links.menu.yml @@ -0,0 +1,3 @@ +tour_example.description: + title: Tour Example + route_name: tour_example_description diff --git a/sites/all/modules/examples/examples/tour_example/tour_example.module b/sites/all/modules/examples/examples/tour_example/tour_example.module new file mode 100644 index 000000000..b033d08e8 --- /dev/null +++ b/sites/all/modules/examples/examples/tour_example/tour_example.module @@ -0,0 +1,38 @@ + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/examples/ms_ajax_form_example/README.txt b/sites/all/modules/examples/ms_ajax_form_example/README.txt new file mode 100644 index 000000000..8bdf49ce9 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/README.txt @@ -0,0 +1 @@ +Multi Step Form Example diff --git a/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.info.yml b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.info.yml new file mode 100644 index 000000000..7343c7eb2 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.info.yml @@ -0,0 +1,12 @@ +name: Multi Step Form Example +type: module +description: 'Defines framework to create ajax based multi step forms.' +# core: 8.x +package: Custom +configure: ms_ajax_form_example.page + +# Information added by Drupal.org packaging script on 2017-05-28 +version: '8.x-1.0' +core: '8.x' +project: 'ms_ajax_form_example' +datestamp: 1495974785 diff --git a/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.routing.yml b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.routing.yml new file mode 100644 index 000000000..e453149aa --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/ms_ajax_form_example.routing.yml @@ -0,0 +1,7 @@ +ms_ajax_form_example.page: + path: '/multi-step-ajax-form' + defaults: + _form: '\Drupal\ms_ajax_form_example\Form\MultiStepExampleForm' + _title: 'Welcome' + requirements: + _permission: 'access content' diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/BaseButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/BaseButton.php new file mode 100644 index 000000000..d6d195f79 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/BaseButton.php @@ -0,0 +1,26 @@ + 'submit', + '#value' => t('Next'), + '#goto_step' => StepsEnum::STEP_TWO, + ]; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreeFinishButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreeFinishButton.php new file mode 100644 index 000000000..3fe50884c --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreeFinishButton.php @@ -0,0 +1,40 @@ + 'submit', + '#value' => t('Finish!'), + '#goto_step' => StepsEnum::STEP_FINALIZE, + '#submit_handler' => 'submitValues', + ]; + } + + /** + * {@inheritdoc} + */ + public function getSubmitHandler() { + return 'submitIntake'; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreePreviousButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreePreviousButton.php new file mode 100644 index 000000000..58ab14429 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepThreePreviousButton.php @@ -0,0 +1,33 @@ + 'submit', + '#value' => t('Previous'), + '#goto_step' => StepsEnum::STEP_TWO, + '#skip_validation' => TRUE, + ]; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoNextButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoNextButton.php new file mode 100644 index 000000000..6289199de --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoNextButton.php @@ -0,0 +1,32 @@ + 'submit', + '#value' => t('Next'), + '#goto_step' => StepsEnum::STEP_THREE, + ]; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoPreviousButton.php b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoPreviousButton.php new file mode 100644 index 000000000..549a5c404 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Button/StepTwoPreviousButton.php @@ -0,0 +1,33 @@ + 'submit', + '#value' => t('Previous'), + '#goto_step' => StepsEnum::STEP_ONE, + '#skip_validation' => TRUE, + ]; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Form/MultiStepExampleForm.php b/sites/all/modules/examples/ms_ajax_form_example/src/Form/MultiStepExampleForm.php new file mode 100644 index 000000000..147cc8fba --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Form/MultiStepExampleForm.php @@ -0,0 +1,206 @@ +stepId = StepsEnum::STEP_ONE; + $this->stepManager = new StepManager(); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ms_ajax_form_example'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['wrapper-messages'] = [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'messages-wrapper', + ], + ]; + + $form['wrapper'] = [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'form-wrapper', + ], + ]; + + // Get step from step manager. + $this->step = $this->stepManager->getStep($this->stepId); + + // Attach step form elements. + $form['wrapper'] += $this->step->buildStepFormElements(); + + // Attach buttons. + $form['wrapper']['actions']['#type'] = 'actions'; + $buttons = $this->step->getButtons(); + foreach ($buttons as $button) { + /** @var \Drupal\ms_ajax_form_example\Button\ButtonInterface $button */ + $form['wrapper']['actions'][$button->getKey()] = $button->build(); + + if ($button->ajaxify()) { + // Add ajax to button. + $form['wrapper']['actions'][$button->getKey()]['#ajax'] = [ + 'callback' => [$this, 'loadStep'], + 'wrapper' => 'form-wrapper', + 'effect' => 'fade', + ]; + } + + $callable = [$this, $button->getSubmitHandler()]; + if ($button->getSubmitHandler() && is_callable($callable)) { + // Attach submit handler to button, so we can execute it later on.. + $form['wrapper']['actions'][$button->getKey()]['#submit_handler'] = $button->getSubmitHandler(); + } + } + + return $form; + + } + + /** + * Ajax callback to load new step. + * + * @param array $form + * Form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state interface. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * Ajax response. + */ + public function loadStep(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + + $messages = drupal_get_messages(); + if (!empty($messages)) { + // Form did not validate, get messages and render them. + $messages = [ + '#theme' => 'status_messages', + '#message_list' => $messages, + '#status_headings' => [ + 'status' => $this->t('Status message'), + 'error' => $this->t('Error message'), + 'warning' => $this->t('Warning message'), + ], + ]; + $response->addCommand(new HtmlCommand('#messages-wrapper', $messages)); + } + else { + // Remove messages. + $response->addCommand(new HtmlCommand('#messages-wrapper', '')); + } + + // Update Form. + $response->addCommand(new HtmlCommand('#form-wrapper', + $form['wrapper'])); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + // Only validate if validation doesn't have to be skipped. + // For example on "previous" button. + if (empty($triggering_element['#skip_validation']) && $fields_validators = $this->step->getFieldsValidators()) { + // Validate fields. + foreach ($fields_validators as $field => $validators) { + // Validate all validators for field. + $field_value = $form_state->getValue($field); + foreach ($validators as $validator) { + if (!$validator->validates($field_value)) { + $form_state->setErrorByName($field, $validator->getErrorMessage()); + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Save filled values to step. So we can use them as default_value later on. + $values = []; + foreach ($this->step->getFieldNames() as $name) { + $values[$name] = $form_state->getValue($name); + } + $this->step->setValues($values); + // Add step to manager. + $this->stepManager->addStep($this->step); + // Set step to navigate to. + $triggering_element = $form_state->getTriggeringElement(); + $this->stepId = $triggering_element['#goto_step']; + + // If an extra submit handler is set, execute it. + // We already tested if it is callable before. + if (isset($triggering_element['#submit_handler'])) { + $this->{$triggering_element['#submit_handler']}($form, $form_state); + } + + $form_state->setRebuild(TRUE); + } + + /** + * Submit handler for last step of form. + * + * @param array $form + * Form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state interface. + */ + public function submitValues(array &$form, FormStateInterface $form_state) { + // Submit all values to DB or do whatever you want on submit. + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Manager/StepManager.php b/sites/all/modules/examples/ms_ajax_form_example/src/Manager/StepManager.php new file mode 100644 index 000000000..2d58d194f --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Manager/StepManager.php @@ -0,0 +1,73 @@ +steps[$step->getStep()] = $step; + } + + /** + * Fetches step from steps property, If it doesn't exist, create step object. + * + * @param int $step_id + * Step ID. + * + * @return \Drupal\ms_ajax_form_example\Step\StepInterface + * Return step object. + */ + public function getStep($step_id) { + if (isset($this->steps[$step_id])) { + // If step was already initialized, use that step. + // Chance is there are values stored on that step. + $step = $this->steps[$step_id]; + } + else { + // Get class. + $class = StepsEnum::map($step_id); + // Init step. + $step = new $class($this); + } + + return $step; + } + + /** + * Get all steps. + * + * @return \Drupal\ms_ajax_form_example\Step\StepInterface + * Steps. + */ + public function getAllSteps() { + return $this->steps; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/BaseStep.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/BaseStep.php new file mode 100644 index 000000000..186926126 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/BaseStep.php @@ -0,0 +1,80 @@ +step = $this->setStep(); + } + + /** + * {@inheritdoc} + */ + public function getStep() { + return $this->step; + } + + /** + * {@inheritdoc} + */ + public function isLastStep() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function setValues($values) { + $this->values = $values; + } + + /** + * {@inheritdoc} + */ + public function getValues() { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function getFieldNames() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getFieldsValidators() { + return []; + } + + /** + * {@inheritdoc} + */ + abstract protected function setStep(); + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepFinalize.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepFinalize.php new file mode 100644 index 000000000..c537e6cc3 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepFinalize.php @@ -0,0 +1,38 @@ + t('You have completed the wizard, yeah!'), + ]; + + return $form; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepInterface.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepInterface.php new file mode 100644 index 000000000..a19da3a08 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepInterface.php @@ -0,0 +1,58 @@ + 'textfield', + '#title' => t("What's your name?"), + '#required' => FALSE, + '#default_value' => isset($this->getValues()['name']) ? $this->getValues()['name'] : NULL, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFieldNames() { + return [ + 'name', + ]; + } + + /** + * {@inheritdoc} + */ + public function getFieldsValidators() { + return [ + 'name' => [ + new ValidatorRequired("Hey stranger, please tell me your name. I would like to get to know you."), + ], + ]; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepThree.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepThree.php new file mode 100644 index 000000000..9d5771b66 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepThree.php @@ -0,0 +1,70 @@ + 'textfield', + '#title' => t('What is your LinkedIn URL?'), + '#default_value' => isset($this->getValues()['linkedin']) ? $this->getValues()['linkedin'] : NULL, + '#required' => FALSE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFieldNames() { + return [ + 'linkedin', + ]; + } + + /** + * {@inheritdoc} + */ + public function getFieldsValidators() { + return [ + 'linkedin' => [ + new ValidatorRequired("Tell me where I can find your LinkedIn please."), + new ValidatorRegex(t("I don't think this is a valid LinkedIn URL..."), '/(ftp|http|https):\/\/(.*)linkedin(.*)/'), + ], + ]; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepTwo.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepTwo.php new file mode 100644 index 000000000..2fe1eb332 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepTwo.php @@ -0,0 +1,68 @@ + 'checkboxes', + '#title' => t('Nice to meet you! So, what are you interests?'), + '#options' => [1 => 'interest 1', 2 => 'interest 2', 3 => 'interest 3'], + '#default_value' => isset($this->getValues()['interests']) ? $this->getValues()['interests'] : [], + '#required' => FALSE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFieldNames() { + return [ + 'interests', + ]; + } + + /** + * {@inheritdoc} + */ + public function getFieldsValidators() { + return [ + 'interests' => [ + new ValidatorRequired("It would be a lot easier for me if you could fill out some of your interests."), + ], + ]; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepsEnum.php b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepsEnum.php new file mode 100644 index 000000000..3c1d0288b --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Step/StepsEnum.php @@ -0,0 +1,55 @@ + 'step-one', + self::STEP_TWO => 'step-two', + self::STEP_THREE => 'step-three', + self::STEP_FINALIZE => 'step-finalize', + ]; + } + + /** + * Map steps to it's class. + * + * @param int $step + * Step number. + * + * @return bool + * Return true if exist. + */ + public static function map($step) { + $map = [ + self::STEP_ONE => 'Drupal\\ms_ajax_form_example\\Step\\StepOne', + self::STEP_TWO => 'Drupal\\ms_ajax_form_example\\Step\\StepTwo', + self::STEP_THREE => 'Drupal\\ms_ajax_form_example\\Step\\StepThree', + self::STEP_FINALIZE => 'Drupal\\ms_ajax_form_example\\Step\\StepFinalize', + ]; + + return isset($map[$step]) ? $map[$step] : FALSE; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Validator/BaseValidator.php b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/BaseValidator.php new file mode 100644 index 000000000..c7a663cdb --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/BaseValidator.php @@ -0,0 +1,31 @@ +errorMessage = $error_message; + } + + /** + * {@inheritdoc} + */ + public function getErrorMessage() { + return $this->errorMessage; + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorInterface.php b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorInterface.php new file mode 100644 index 000000000..d8512fe29 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorInterface.php @@ -0,0 +1,22 @@ +pattern = $pattern; + } + + /** + * {@inheritdoc} + */ + public function validates($value) { + return preg_match($this->pattern, $value); + } + +} diff --git a/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRequired.php b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRequired.php new file mode 100644 index 000000000..ac0b2ab54 --- /dev/null +++ b/sites/all/modules/examples/ms_ajax_form_example/src/Validator/ValidatorRequired.php @@ -0,0 +1,19 @@ +