├── README.md └── code_2nd_ed ├── .DS_Store ├── drupal7 ├── mymodule │ ├── README.txt │ ├── mymodule-hookname.tpl.php │ ├── mymodule.info │ ├── mymodule.install │ ├── mymodule.js │ ├── mymodule.module │ ├── mymodule.rules.inc │ ├── mymodule.rules_defaults.inc │ ├── plugins │ │ └── ctools-relationships │ │ │ └── mymodule_relationship_most_recent_content.inc │ ├── rules │ │ └── sample_rule.txt │ └── tests │ │ └── mymodule.test └── mytheme │ ├── README.txt │ ├── mytheme.info │ └── templates │ └── page.tpl.php └── drupal8 ├── mymodule ├── README.txt ├── config │ ├── install │ │ └── mymodule.settings.yml │ ├── optional │ │ └── views.view.myentity_content_list.yml │ └── schema │ │ └── mymodule.schema.yml ├── mymodule.info.yml ├── mymodule.install ├── mymodule.js ├── mymodule.libraries.yml ├── mymodule.links.action.yml ├── mymodule.links.menu.yml ├── mymodule.links.task.yml ├── mymodule.module ├── mymodule.permissions.yml ├── mymodule.routing.yml ├── mymodule.services.yml ├── src │ ├── Controller │ │ └── MyUrlController.php │ ├── Entity │ │ ├── MyEntity.php │ │ ├── MyEntityDeleteForm.php │ │ ├── MyEntityForm.php │ │ ├── MyEntityInterface.php │ │ ├── MyEntityType.php │ │ ├── MyEntityTypeDeleteForm.php │ │ ├── MyEntityTypeForm.php │ │ ├── MyEntityTypeInterface.php │ │ ├── MyEntityTypeListBuilder.php │ │ └── MyEntityViewsData.php │ ├── Form │ │ ├── ConfirmDeleteForm.php │ │ └── PersonalDataForm.php │ ├── Plugin │ │ ├── Block │ │ │ └── MyModuleFirstBlock.php │ │ ├── Field │ │ │ ├── FieldFormatter │ │ │ │ └── MyCustomText.php │ │ │ └── FieldWidget │ │ │ │ └── MyCustomText.php │ │ └── views │ │ │ └── wizard │ │ │ └── MyEntityViewsWizard.php │ ├── Routing │ │ └── MyModuleRouting.php │ └── Tests │ │ ├── MyModuleEnableTest.php │ │ ├── MyModuleEntityTest.php │ │ ├── MyModulePageTest.php │ │ ├── MyModuleSnippetsTest.php │ │ ├── MyThemeTest.php │ │ └── ProgrammersGuideTestBase.php └── templates │ ├── myentity.html.twig │ └── mymodule-hookname.html.twig └── mytheme ├── README.txt ├── mytheme.info.yml └── templates └── page.html.twig /README.md: -------------------------------------------------------------------------------- 1 | Programmer's Guide to Drupal 2 | ============================ 3 | 4 | This is the example code that accompanies 5 | [Programmer's Guide to Drupal, Second edition](http://shop.oreilly.com/product/0636920034612.do), by 6 | [Jennifer Hodgdon](https://github.com/jhodgdon-drp) (ISBN: 978-1-4919-1146-4). 7 | 8 | Click the Download Zip button on this page to download the code. 9 | 10 | See an error? 11 | [Report errors here](http://oreilly.com/catalog/errata.csp?isbn=0636920034612), 12 | or simply fork and send us a pull request. 13 | -------------------------------------------------------------------------------- /code_2nd_ed/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreillymedia/programmers_guide_to_drupal/38af1d88ba559579a6e5d7065e65a45f9e87fec2/code_2nd_ed/.DS_Store -------------------------------------------------------------------------------- /code_2nd_ed/drupal7/mymodule/README.txt: -------------------------------------------------------------------------------- 1 | This directory contains the Drupal module 7 code from the book "Programmers 2 | Guide to Drupal", by Jennifer Hodgdon, second edition, 2014. 3 | 4 | License: See book for details of license and copyright. 5 | 6 | Dependencies: 7 | 8 | Some of the code in the sample module depends on the contributed Entity API 9 | module, so you cannot enable this module without it. Some of the other code is 10 | for integration with Rules, Panels, and CTools, but as it doesn't run without 11 | those modules installed, they are not listed as dependencies of this 12 | module. Modules mentioned: 13 | - https://drupal.org/project/ctools 14 | - https://drupal.org/project/entity 15 | - https://drupal.org/project/panels 16 | - https://drupal.org/project/rules 17 | 18 | Contents of this directory: 19 | 20 | - mymodule.module: Module file containing code from many sections of the book. 21 | - mymodule.info: Info file for module. 22 | - mymodule.install: Install hooks for module, from several sections of the book. 23 | 24 | - mymodule.js: Empty JavaScript file for inclusion in a form array. 25 | 26 | - mymodule.tpl.php: Theme template file from section "Making Your Output 27 | Themeable", Chapter 2. 28 | 29 | - mymodule.rules.inc: Rules action from section "Providing Custom Actions to 30 | Rules", chapter 4. 31 | - mymodule.rules_defaults.inc: Default rule provision from section "Providing 32 | Default Reaction Rules and Components", chapter 4. 33 | - rules/sample_rule.txt: Exported sample rule for default rule provision. 34 | 35 | - plugins/ctools-relationships/mymodule_relationship_most_recent_content.inc: 36 | CTools relationship plugin from "Implementing CTools Plugins in Drupal 7", 37 | chapter 4. 38 | 39 | - tests/mymodule.test: SimpleTest test classes for code from the book. 40 | 41 | Note: There is some functionality in the sample code that involves 42 | JavaScript, and therefore cannot be tested within the SimpleTest framework. 43 | To test it manually, you'll need to put this module in your sites/all/modules 44 | directory, enable the "My Module" module, and do the following: 45 | 46 | 1. On the Personal Data Form page at example.com/mymodule/my_form_page : 47 | a. When you visit the page, there should be a JavaScript alert box saying 48 | "Hello!". 49 | b. The field labeled "Autocomplete field" should have auto-complete behavior. 50 | It is set up with a proxy that just adds some fixed text after 51 | whatever you type in, so you should see some choices. You will need 52 | to have 'Use company field' permission to see the auto-complete. 53 | c. If you type in the field labeled "Type here to trigger Ajax", you should 54 | see a message saying "You have triggered Ajax", and just below that, a 55 | line saying "You typed [whatever you typed]". 56 | d. If you click the button that says "Click here to trigger Ajax", you 57 | should see a message saying "The button has been clicked", with a 58 | light green background. 59 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal7/mymodule/mymodule-hookname.tpl.php: -------------------------------------------------------------------------------- 1 | 16 |
17 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal7/mymodule/mymodule.info: -------------------------------------------------------------------------------- 1 | ; Drupal 7 sample module .info file 2 | 3 | ; Comments start with ; 4 | 5 | ; The name displayed on the Modules list page. 6 | name = My Module 7 | 8 | ; The longer description displayed on the Modules list. 9 | description = Sample module from Programmers Guide to Drupal 10 | 11 | ; The Drupal core version this module is compatible with. 12 | core = 7.x 13 | 14 | ; Dependencies on other modules. 15 | dependencies[] = node 16 | dependencies[] = entity 17 | 18 | ; Files that Drupal needs to be notified of. 19 | files[] = mymodule-hookname.tpl.php 20 | files[] = tests/mymodule.test 21 | 22 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal7/mymodule/mymodule.install: -------------------------------------------------------------------------------- 1 | t('Some appropriate name'), 20 | 'machine_name' => 'mymodule_appropriate_name', 21 | 'description' => t('Some appropriate description'), 22 | 'module' => 'mymodule', 23 | ); 24 | taxonomy_vocabulary_save($vocabulary); 25 | variable_set('mymodule_vocabulary', $vocabulary->vid); 26 | } 27 | } 28 | 29 | /** 30 | * Implements hook_schema(). 31 | * 32 | * 'mymodule_foo' table is from "Setting up Database Tables", Chapter 2. 33 | * 34 | * 'mymodule_myentity' table is from "Defining an Entity Type in Drupal 7", 35 | * chapter 4. 36 | */ 37 | function mymodule_schema() { 38 | $schema = array(); 39 | 40 | // Define a simple table. 41 | $schema['mymodule_foo'] = array( 42 | 'description' => 'Untranslated description of this table', 43 | 'fields' => array( 44 | 'bar' => array( 45 | 'description' => 'Untranslated description of this field', 46 | 'type' => 'varchar', 47 | 'length' => 50, 48 | 'default' => '', 49 | ), 50 | 'baz' => array( 51 | 'description' => 'Untranslated description of this field', 52 | 'type' => 'int', 53 | 'unsigned' => TRUE, 54 | 'default' => 0, 55 | ), 56 | ), 57 | 'primary key' => array('baz'), 58 | ); 59 | 60 | // Define a table for entity storage. 61 | $schema['mymodule_myentity'] = array( 62 | 'description' => 'Storage for myentity entity: settings for mymodule', 63 | 'fields' => array( 64 | 'myentity_id' => array( 65 | 'description' => 'Primary key: settings ID.', 66 | 'type' => 'serial', 67 | 'unsigned' => TRUE, 68 | 'not null' => TRUE, 69 | ), 70 | 'title' => array( 71 | 'description' => 'Label assigned to this set of settings', 72 | 'type' => 'varchar', 73 | 'length' => 200, 74 | 'default' => '', 75 | ), 76 | 'language' => array( 77 | 'description' => 'Language of this set of settings', 78 | 'type' => 'varchar', 79 | 'length' => 12, 80 | 'not null' => TRUE, 81 | 'default' => '', 82 | ), 83 | // Consider adding additional fields for time created, time updated. 84 | ), 85 | 86 | 'primary key' => array('myentity_id'), 87 | 88 | 'indexes' => array( 89 | 'language' => array('language'), 90 | // Add indexes for created/updated here too. 91 | ), 92 | ); 93 | 94 | return $schema; 95 | } 96 | 97 | /** 98 | * Make one field wider and add a new field in the mymodule_foo table. 99 | * 100 | * From Setting up Database Tables section in Chapter 2. 101 | * 102 | * Note: If you actually want to run this update, change the function name 103 | * to remove the "not_", and then visit example.com/update.php. 104 | */ 105 | function not_mymodule_update_7001() { 106 | db_change_field('mymodule_foo', 'bar', 'bar', array( 107 | 'description' => 'Untranslated description of this field', 108 | 'type' => 'varchar', 109 | 'length' => 150, 110 | 'default' => '', 111 | )); 112 | db_add_field('mymodule_foo', 'bay', array( 113 | 'description' => 'Untranslated description of this field', 114 | 'type' => 'varchar', 115 | 'length' => 50, 116 | 'default' => '', 117 | )); 118 | } 119 | 120 | /** 121 | * Implements hook_install(). 122 | * 123 | * From "Defining an Entity Type in Drupal 7", chapter 4. Adds two fields to 124 | * this internal-use entity type. 125 | */ 126 | function mymodule_install() { 127 | // Create a plain text field for a setting. 128 | $field = field_create_field(array( 129 | 'field_name' => 'myentity_setting_1', 130 | 'type' => 'text', 131 | 'entity_types' => array('myentity'), 132 | 'locked' => TRUE, 133 | 'translatable' => TRUE, 134 | )); 135 | 136 | // Attach the field to the entity bundle. 137 | $instance = field_create_instance(array( 138 | 'field_name' => 'myentity_setting_1', 139 | 'entity_type' => 'myentity', 140 | 'bundle' => 'myentity', 141 | 'label' => t('Setting 1'), 142 | 'description' => t('Help for this setting'), 143 | 'required' => TRUE, 144 | 'widget' => array( 145 | 'type' => 'text_textfield', 146 | ), 147 | 'display' => array( 148 | 'default' => array( 149 | 'label' => 'above', 150 | 'type' => 'text_default', 151 | ), 152 | ), 153 | )); 154 | 155 | // Repeat these two function calls for each additional field. 156 | } 157 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal7/mymodule/mymodule.js: -------------------------------------------------------------------------------- 1 | // JavaScript file (empty) for this module. 2 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal7/mymodule/mymodule.module: -------------------------------------------------------------------------------- 1 | t('Hello World!'), 17 | * '#theme' => 'mymodule_hookname', 18 | * ); 19 | * @endcode 20 | * 21 | * To use the second theme hook in a call to theme() directly: 22 | * @code 23 | * $data = array('input1' => t('Hello World!')); 24 | * $output = theme('mymodule_hookname_2', $data); 25 | * @endcode 26 | * 27 | * @see theme_mymodule_hookname_2() 28 | * @see mymodule-hookname.tpl.php 29 | */ 30 | function mymodule_theme($existing, $type, $theme, $path) { 31 | return array( 32 | // The array keys are names of the theme hooks you are defining. 33 | 'mymodule_hookname' => array( 34 | // Input variables. 35 | 'variables' => array( 36 | // These are passed as an array to theme(), which passes them on 37 | // to your theme function or template. Here, provide default values. 38 | 'input1' => '', 39 | ), 40 | // If you want to use a template, include this line; for a theme 41 | // function, leave it out. 42 | 'template' => 'mymodule-hookname', 43 | ), 44 | 45 | // Define a second theme hook, using a theme function. 46 | 'mymodule_hookname_2' => array( 47 | 'variables' => array( 48 | 'input1' => '', 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | /** 55 | * Sample theme function to go with the hook_theme() implementation. 56 | * 57 | * This produces the same output as mymodule-hookname.tpl.php, but using 58 | * a theme function. 59 | * 60 | * From section "Making Your Output Themeable" in Chapter 2. 61 | * 62 | * @see mymodule_theme() 63 | * @see mymodule-hookname.tpl.php 64 | */ 65 | function theme_mymodule_hookname_2($variables) { 66 | return '' . t('General information goes here.') . '
', 295 | ), 296 | 297 | // List of items, using the theme hook 'item_list'. 298 | 'colors' => array( 299 | '#theme' => 'item_list', 300 | '#items' => array(t('Red'), t('Blue'), t('Green')), 301 | '#title' => t('Colors'), 302 | ), 303 | 304 | // Table, using the theme hook 'table'. 305 | 'materials' => array( 306 | '#theme' => 'table', 307 | '#caption' => t('Materials'), 308 | '#header' => array(t('Material'), t('Characteristic')), 309 | '#rows' => array( 310 | array(t('Steel'), t('Strong')), 311 | array(t('Aluminum'), t('Light')), 312 | ), 313 | ), 314 | ); 315 | 316 | return $output; 317 | } 318 | 319 | /** 320 | * Form constructor for a personal data form. 321 | * 322 | * From "Basic Form Generation and Processing in Drupal 7", chapter 4. 323 | * 324 | * Most of form array is from "Form Arrays, Form State Arrays, and 325 | * Form State Objects", chapter 4. 326 | * 327 | * Auto-complete field from "Adding Auto-Complete to Forms", chapter 4. 328 | * 329 | * JavaScript attachment from "Creating Render Arrays for Page and Block 330 | * Output", chapter 4. 331 | * 332 | * Ajax fields from "Setting Up a Form for Ajax", chapter 4. 333 | * 334 | * @see mymodule_permission() 335 | * @see mymodule_personal_data_form_submit() 336 | * @see mymodule_autocomplete() 337 | * @see mymodule_ajax_text_callback() 338 | * @see mymodule_ajax_button_callback() 339 | */ 340 | function mymodule_personal_data_form($form, &$form_state) { 341 | // Define the form array. 342 | 343 | // Plain text input element for first name. 344 | $form['first_name'] = array( 345 | '#type' => 'textfield', 346 | '#title' => t('First name'), 347 | ); 348 | 349 | // Plain text element for company name, only visible to some 350 | // users. 351 | $form['company'] = array( 352 | '#type' => 'textfield', 353 | '#title' => t('Company'), 354 | // This assumes permission 'use company field' has been defined. 355 | '#access' => user_access('use company field'), 356 | ); 357 | 358 | $my_information = 'stuff'; 359 | 360 | // Some hidden information to be used later. 361 | $form['information'] = array( 362 | '#type' => 'value', 363 | '#value' => $my_information, 364 | ); 365 | 366 | // Submit button. 367 | $form['submit'] = array( 368 | '#type' => 'submit', 369 | '#value' => t('Submit'), 370 | ); 371 | 372 | // Attach a JavaScript file. 373 | $form['#attached']['js'][] = 374 | drupal_get_path('module', 'mymodule') . '/mymodule.js'; 375 | 376 | // Attach some in-line JavaScript code. 377 | $my_code = 'alert("Hello!");'; 378 | $form['#attached']['js'][] = array( 379 | 'type' => 'inline', 380 | 'data' => $my_code, 381 | ); 382 | 383 | // Auto-complete field. 384 | $form['my_autocomplete_field'] = array( 385 | '#type' => 'textfield', 386 | '#title' => t('Autocomplete field'), 387 | '#autocomplete_path' => 'mymodule/autocomplete', 388 | ); 389 | 390 | // Ajax elements. 391 | 392 | $form['ajax_output_1'] = array( 393 | '#type' => 'markup', 394 | '#markup' => '', 395 | ); 396 | 397 | $form['text_trigger'] = array( 398 | '#type' => 'textfield', 399 | '#title' => t('Type here to trigger Ajax'), 400 | '#ajax' => array( 401 | 'event' => 'keyup', 402 | 'wrapper' => 'ajax-output-spot', 403 | 'callback' => 'mymodule_ajax_text_callback', 404 | ), 405 | ); 406 | 407 | $form['ajax_output_2'] = array( 408 | '#type' => 'markup', 409 | '#markup' => '', 410 | ); 411 | 412 | $form['button_trigger'] = array( 413 | '#type' => 'button', 414 | '#value' => t('Click here to trigger Ajax'), 415 | '#ajax' => array( 416 | 'callback' => 'mymodule_ajax_button_callback', 417 | ), 418 | ); 419 | 420 | return $form; 421 | } 422 | 423 | /** 424 | * Form submission handler for mymodule_personal_data_form(). 425 | * 426 | * From "Basic Form Generation and Processing in Drupal 7", chapter 4. 427 | */ 428 | function mymodule_personal_data_form_submit(&$form, &$form_state) { 429 | // The values submitted by the user are in $form_state['values']. 430 | $name = $form_state['values']['first_name']; 431 | // Values you stored in the form array are also available. 432 | $info = $form_state['values']['information']; 433 | 434 | // Get another value, using a method where it could be nested. 435 | $parents = $form['company']['#array_parents']; 436 | $company = drupal_array_get_nested_value($form_state['values'], $parents); 437 | 438 | // Processing code would go here. As a proxy, display a message with the 439 | // values. Note that since the values are unsanitized, insert them 440 | // into t() with @variable. If inserting into the database, do not 441 | // sanitize. 442 | if ($company) { 443 | drupal_set_message(t('Thank you @name from @company', array('@name' => $name, '@company' => $company))); 444 | } 445 | else { 446 | drupal_set_message(t('Thank you @name', array('@name' => $name))); 447 | } 448 | 449 | // Make sure the form is rebuilt properly for Ajax. 450 | $form_state['rebuild'] = TRUE; 451 | } 452 | 453 | /** 454 | * Generates autocompletes for path mymodule/autocomplete. 455 | * 456 | * From "Adding Auto-Complete to Forms", chapter 4. 457 | * 458 | * @see mymodule_personal_data_form() 459 | */ 460 | function mymodule_autocomplete($string = '') { 461 | $matches = array(); 462 | if ($string) { 463 | // Sanitize $string and find appropriate matches -- about 10 or fewer. 464 | // Put them into $matches as $key => $visible text. 465 | $string = check_plain($string); 466 | 467 | // As a proxy, just add some text to the end of the submitted text. 468 | $additions = array('add', 'choice', 'more', 'plus', 'something'); 469 | foreach ($additions as $word) { 470 | $choice = $string . $word; 471 | $matches[$choice] = $choice; 472 | } 473 | } 474 | 475 | drupal_json_output($matches); 476 | } 477 | 478 | /** 479 | * Processes the Ajax response for the text field. 480 | * 481 | * From "Wrapper-based Ajax Callback Functions", chapter 4. 482 | * 483 | * @see mymodule_personal_data_form() 484 | */ 485 | function mymodule_ajax_text_callback($form, &$form_state) { 486 | // Read the text from the text field. 487 | $text = $form_state['values']['text_trigger']; 488 | if (!$text) { 489 | $text = t('nothing'); 490 | } 491 | 492 | // Set a message. 493 | drupal_set_message(t('You have triggered Ajax')); 494 | 495 | // Return a render array for markup to replace the wrapper' . t('Predefined output text x') . '
'); 848 | break; 849 | 850 | case 'y_stored': 851 | // Output the corresponding text or icon. 852 | $output[$delta] = array('#markup' => '' . t('Predefined output text y') . '
'); 853 | break; 854 | 855 | // Handle other options here. 856 | } 857 | } 858 | } 859 | 860 | return $output; 861 | } 862 | 863 | /** 864 | * Implements hook_ctools_plugin_directory(). 865 | * 866 | * From "Notifying CTools About Plugin Implementations" in chapter 4. 867 | */ 868 | function mymodule_ctools_plugin_directory($module, $plugin) { 869 | // Define a directory structure for plugins, so for instance a 870 | // 'relationships' plugin (type defined by the 'ctools' module) would 871 | // go into directory plugins/ctools-relationships. 872 | return 'plugins/' . $module . '-' . $plugin; 873 | } 874 | 875 | /** 876 | * Implements hook_ctools_plugin_api(). 877 | * 878 | * From "Providing Default CTools Exportables", chapter 4. 879 | */ 880 | function mymodule_ctools_plugin_api() { 881 | return array( 882 | // The API version. 883 | 'api' => 1, 884 | ); 885 | } 886 | 887 | /** 888 | * Implements hook_default_panels_mini(). 889 | * 890 | * From "Providing Default CTools Exportables", chapter 4. 891 | */ 892 | function mymodule_default_panels_mini() { 893 | $minis = array(); 894 | 895 | // Paste exported Mini Panel code here. 896 | // --- Start of export --- 897 | $mini = new stdClass(); 898 | $mini->disabled = FALSE; /* Edit this to true to make a default mini disabled initially */ 899 | $mini->api_version = 1; 900 | $mini->name = 'mymodule_test'; 901 | $mini->category = ''; 902 | $mini->admin_title = 'My Module Test'; 903 | $mini->admin_description = ''; 904 | $mini->requiredcontexts = array(); 905 | $mini->contexts = array( 906 | 0 => array( 907 | 'identifier' => 'User', 908 | 'keyword' => 'user', 909 | 'name' => 'user', 910 | 'type' => 'current', 911 | 'uid' => '', 912 | 'id' => 1, 913 | ), 914 | ); 915 | $mini->relationships = array( 916 | 0 => array( 917 | 'identifier' => 'Most recent node', 918 | 'keyword' => 'node', 919 | 'name' => 'mymodule_relationship_most_recent_content', 920 | 'context' => 'context_user_1', 921 | 'id' => 1, 922 | ), 923 | ); 924 | $display = new panels_display(); 925 | $display->layout = 'onecol'; 926 | $display->layout_settings = array(); 927 | $display->panel_settings = array( 928 | 'style_settings' => array( 929 | 'default' => NULL, 930 | 'middle' => NULL, 931 | ), 932 | ); 933 | $display->cache = array(); 934 | $display->title = 'Most Recently Wrote'; 935 | $display->uuid = '521e9c79-65b2-484f-ba30-b398be1f3987'; 936 | $display->content = array(); 937 | $display->panels = array(); 938 | $pane = new stdClass(); 939 | $pane->pid = 'new-c6ad4d39-3c42-4689-a5ff-4f75594a06df'; 940 | $pane->panel = 'middle'; 941 | $pane->type = 'node_title'; 942 | $pane->subtype = 'node_title'; 943 | $pane->shown = TRUE; 944 | $pane->access = array(); 945 | $pane->configuration = array( 946 | 'link' => 1, 947 | 'markup' => 'none', 948 | 'id' => '', 949 | 'class' => '', 950 | 'context' => 'relationship_mymodule_relationship_most_recent_content_1', 951 | 'override_title' => 0, 952 | 'override_title_text' => '', 953 | ); 954 | $pane->cache = array(); 955 | $pane->style = array( 956 | 'settings' => NULL, 957 | ); 958 | $pane->css = array(); 959 | $pane->extras = array(); 960 | $pane->position = 0; 961 | $pane->locks = array(); 962 | $pane->uuid = 'c6ad4d39-3c42-4689-a5ff-4f75594a06df'; 963 | $display->content['new-c6ad4d39-3c42-4689-a5ff-4f75594a06df'] = $pane; 964 | $display->panels['middle'][0] = 'new-c6ad4d39-3c42-4689-a5ff-4f75594a06df'; 965 | $display->hide_title = PANELS_TITLE_FIXED; 966 | $display->title_pane = '0'; 967 | $mini->display = $display; 968 | // --- End of export --- 969 | 970 | // After the export is pasted, you'll have $mini holding one exported 971 | // mini panel. Put it into the return array. 972 | $minis['mymodule_test'] = $mini; 973 | 974 | // Add additional mini panels here. 975 | 976 | // Return them all. 977 | return $minis; 978 | } 979 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal7/mymodule/mymodule.rules.inc: -------------------------------------------------------------------------------- 1 | t('Load a list of users related to content'), 26 | 'group' => t('My Module custom'), 27 | 28 | // Describe the parameters. 29 | 'parameter' => array( 30 | 31 | 'item' => array( 32 | 'label' => t('Content item to use'), 33 | 'type' => 'node', 34 | ), 35 | 36 | // You can add additional parameters here. 37 | 38 | ), 39 | 40 | // Describe the output. 41 | 'provides' => array( 42 | 'user_list' => array( 43 | 'type' => 'list' . print_r($var, TRUE), ''); 20 | } 21 | 22 | /** 23 | * Verbose output of HTML code with optional label. 24 | */ 25 | public function outputHTML($var, $label = '') { 26 | $this->verbose( 27 | (($label) ? '
' . $this->t('General information goes here.') . '
', 37 | ), 38 | 39 | // List of items, using the theme hook 'item_list'. 40 | 'colors' => array( 41 | '#theme' => 'item_list', 42 | '#items' => array($this->t('Red'), $this->t('Blue'), $this->t('Green')), 43 | '#title' => $this->t('Colors'), 44 | ), 45 | 46 | // Table, using the theme hook 'table'. 47 | 'materials' => array( 48 | '#theme' => 'table', 49 | '#caption' => $this->t('Materials'), 50 | '#header' => array($this->t('Material'), $this->t('Characteristic')), 51 | '#rows' => array( 52 | array($this->t('Steel'), $this->t('Strong')), 53 | array($this->t('Aluminum'), $this->t('Light')), 54 | ), 55 | ), 56 | ); 57 | 58 | return $output; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal8/mymodule/src/Plugin/Field/FieldFormatter/MyCustomText.php: -------------------------------------------------------------------------------- 1 | $item) { 35 | // See which option was selected. 36 | switch ($item->value) { 37 | 38 | case 'x_stored': 39 | // Output the corresponding text or icon. 40 | $output[$delta] = array('#markup' => '' . $this->t('Predefined output text x') . '
'); 41 | break; 42 | 43 | case 'y_stored': 44 | // Output the corresponding text or icon. 45 | $output[$delta] = array('#markup' => '' . $this->t('Predefined output text y') . '
'); 46 | break; 47 | 48 | // Handle other options here. 49 | } 50 | } 51 | return $output; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal8/mymodule/src/Plugin/Field/FieldWidget/MyCustomText.php: -------------------------------------------------------------------------------- 1 | value) ? $items[$delta]->value : NULL; 36 | 37 | // Set up the editing form element. Substitute your custom 38 | // code here, instead of using an HTML select. 39 | $element['value'] = $element + array( 40 | '#type' => 'select', 41 | '#options' => array( 42 | 'x_stored' => $this->t('x label'), 43 | 'y_stored' => $this->t('y label'), 44 | ), 45 | '#default_value' => $value, 46 | ); 47 | 48 | return $element; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal8/mymodule/src/Plugin/views/wizard/MyEntityViewsWizard.php: -------------------------------------------------------------------------------- 1 | get('entity.user.collection'); 27 | $route->setDefault('_title', 'User accounts'); 28 | // Make sure that the title text is translatable. 29 | $foo = t('User accounts'); 30 | 31 | // Add a dynamic route at admin/people/mymodule, which could have been 32 | // a static route in this case. 33 | $path = $route->getPath(); 34 | // Constructor parameters: path, defaults, requirements, as you would have 35 | // in a routing.yml file. 36 | $newroute = new Route($path . '/mymodule', array( 37 | '_controller' => '\Drupal\mymodule\Controller\MyUrlController::generateMyPage', 38 | '_title' => 'New page title', 39 | ), array( 40 | '_permission' => 'administer mymodule', 41 | )); 42 | // Make sure that the title text is translatable. 43 | $foo = t('New page title'); 44 | $collection->add('mymodule.newroutename', $newroute); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal8/mymodule/src/Tests/MyModuleEnableTest.php: -------------------------------------------------------------------------------- 1 | insert('mymodule_foo') 37 | ->fields(array('bar' => 'Hello', 'baz' => 2)) 38 | ->execute(); 39 | $result = $connection->query('SELECT * from {mymodule_foo}'); 40 | $count = 0; 41 | foreach ($result as $record) { 42 | $count++; 43 | $this->outputVariable($record, 'Database record'); 44 | $this->assertEqual($record->bar, 'Hello', 'Field bar is correct'); 45 | $this->assertEqual($record->baz, 2, 'Field baz is correct'); 46 | } 47 | $this->assertEqual($count, 1, 'Count of records is correct'); 48 | } 49 | 50 | /** 51 | * Tests the permission hook. 52 | * 53 | * @see mymodule_permission() 54 | */ 55 | function testPermission() { 56 | $account_yes = $this->drupalCreateUser(array('administer mymodule')); 57 | $account_no = $this->drupalCreateUser(array('access content')); 58 | 59 | $this->drupalLogin($account_yes); 60 | $current = \Drupal::currentUser(); 61 | $this->assertTrue($current->hasPermission('administer mymodule'), 'First user has permission'); 62 | 63 | $this->drupalLogin($account_no); 64 | $current = \Drupal::currentUser(); 65 | $this->assertFalse($current->hasPermission('administer mymodule'), 'Second user does not have permission'); 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal8/mymodule/src/Tests/MyModuleEntityTest.php: -------------------------------------------------------------------------------- 1 | drupalPlaceBlock('local_tasks_block'); 29 | $this->drupalPlaceBlock('local_actions_block'); 30 | $this->drupalPlaceBlock('page_title_block'); 31 | } 32 | 33 | /** 34 | * Tests the defined entity type, field formatter, field widget, and views. 35 | * 36 | * @see \Drupal\mymodule\Entity\MyEntity 37 | * @see \Drupal\mymodule\Entity\MyEntitytype 38 | * @see \Drupal\mymodule\Plugin\Field\FieldFormatter\MyCustomText 39 | * @see \Drupal\mymodule\Plugin\Field\FieldWidget\MyCustomText 40 | */ 41 | function testEntityAndField() { 42 | // Log in. 43 | $account = $this->drupalCreateUser(array('administer my entities', 'access administration pages', 'administer myentity fields', 'administer myentity form display', 'administer myentity display', 'administer views')); 44 | $this->drupalLogin($account); 45 | 46 | // Verify that the admin link is on the Structure page. 47 | $this->drupalGet('admin/structure'); 48 | $this->assertLink('My entity subtypes', 0, 'Admin link is shown on Structure'); 49 | $this->assertText('Manage my entity subtypes and their fields', 'Admin description is shown on Structure'); 50 | 51 | // Create an entity sub-type/bundle. 52 | $this->clickLink('My entity subtypes'); 53 | $this->assertTitleContains('My entity subtypes', 'Admin page title is correct'); 54 | $this->clickLink('Add my entity subtype'); 55 | $this->assertText('Machine-readable name', 'Machine name label is present'); 56 | $this->assertText('Label', 'Label label is present'); 57 | $this->assertText('Description', 'Description label is present'); 58 | $this->assertText('Settings', 'Settings fieldset label is present'); 59 | $this->assertText('Published by default', 'Published label is present'); 60 | $this->drupalPostForm(NULL, array( 61 | 'id' => 'test', 62 | 'label' => 'My subtype', 63 | 'description' => 'This is the description', 64 | ), t('Save')); 65 | $this->assertText('My subtype', 'Label is shown'); 66 | $this->assertText('This is the description', 'Description is shown'); 67 | 68 | // Add a text field. 69 | $this->clickLink('Manage fields'); 70 | $this->assertLink('Edit', 0, 'Edit link is present'); 71 | $this->assertLink('Manage form display', 0, 'Form link is present'); 72 | $this->assertLink('Manage display', 0, 'Display link is present'); 73 | 74 | $this->clickLink('Add field'); 75 | $this->drupalPostForm(NULL, array( 76 | 'new_storage_type' => 'string', 77 | 'label' => 'The field label', 78 | 'field_name' => 'abcdef', 79 | ), t('Save and continue')); 80 | $this->drupalPostForm(NULL, array(), t('Save field settings')); 81 | $this->drupalPostForm(NULL, array( 82 | 'description' => 'Some kind of a description', 83 | ), t('Save settings')); 84 | 85 | // Set up the widget. 86 | $this->drupalPostForm('admin/structure/myentity_type/manage/test/form-display', array( 87 | 'fields[field_abcdef][type]' => 'mymodule_mywidget', 88 | ), t('Save')); 89 | 90 | // Set up the formatter. 91 | $this->drupalPostForm('admin/structure/myentity_type/manage/test/display', array( 92 | 'fields[field_abcdef][type]' => 'mymodule_myformatter', 93 | ), t('Save')); 94 | 95 | // Add an entity item. 96 | $this->drupalGet('admin/structure/myentity_type'); 97 | $this->clickLink('Add new My Entity'); 98 | $this->assertTitleContains('Add new my entity', 'Title is correct'); 99 | $this->assertText('Title', 'Label for title field is present'); 100 | $this->assertText('The field label', 'Label for custom field is present'); 101 | $this->assertText('Some kind of a description', 'Description for custom field is present'); 102 | $this->assertRaw(t('x label'), 'X choice label is present from widget'); 103 | $this->assertRaw(t('y label'), 'Y choice label is present from widget'); 104 | $this->drupalPostForm(NULL, array( 105 | 'title[0][value]' => 'My title goes here', 106 | 'field_abcdef[0][value]' => 'x_stored', 107 | ), t('Save')); 108 | 109 | // Test text/link on entity page. 110 | $this->assertTitleContains('My title goes here', 'Entity object title is correct'); 111 | $this->assertText('Predefined output text x', 'Custom field is displayed with custom display'); 112 | $this->assertText(t('View'), 'View tab is there'); 113 | 114 | // Edit the entity. 115 | $this->clickLink(t('Edit')); 116 | $this->assertTitleContains('Edit My title goes here', 'Title is correct'); 117 | $this->assertLink(t('Delete'), 0, 'Delete link is present'); 118 | $this->drupalPostForm(NULL, array( 119 | 'title[0][value]' => 'My new title', 120 | ), t('Save')); 121 | $this->assertTitleContains('My new title', 'Entity edited title is correct'); 122 | // At this point, test Views integration, by using the wizard. 123 | $this->drupalGet('admin/structure/views/add'); 124 | $this->assertRaw('My Entity'); 125 | $this->drupalPostForm(NULL, array( 126 | 'show[wizard_key]' => 'myentity', 127 | 'label' => 'Foo Bar', 128 | 'id' => 'foo_bar', 129 | ), t('Save and edit')); 130 | // Do some reality checks to verify we created a view. 131 | $this->assertText('Foo Bar'); 132 | $this->assertText('My entity'); 133 | $this->assertText('Master'); 134 | $this->assertText('Displays'); 135 | // Click the add fields link and verify that the field is available, 136 | // as well as the default fields for this entity. 137 | $this->drupalGet('admin/structure/views/nojs/add-handler/foo_bar/default/field'); 138 | $this->assertText('My entity'); 139 | $this->assertText('The field label'); 140 | $this->assertText('My entity ID'); 141 | $this->assertText('Subtype'); 142 | $this->assertNoText('Error: missing help'); 143 | 144 | // Visit the page of the default view that is included in this module. 145 | // Verify that this entity is listed. 146 | $this->drupalGet('admin/my-entity-content'); 147 | $this->assertText('My Entity Content'); 148 | $this->assertLink('My new title'); 149 | $this->assertText('My subtype'); 150 | $this->assertLink('Edit'); 151 | $this->assertLink('Delete'); 152 | 153 | // Add another entity item and delete it. 154 | $this->drupalGet('admin/structure/myentity_type'); 155 | $this->clickLink('Add new My Entity'); 156 | $this->drupalPostForm(NULL, array( 157 | 'title[0][value]' => 'whatever', 158 | ), t('Save')); 159 | $this->drupalPostForm('myentity/2/delete', array(), t('Delete')); 160 | $this->drupalget('myentity/2'); 161 | $this->assertResponse('404', 'After deleting, page does not exist'); 162 | 163 | // Delete the entity subtype completely. 164 | $this->drupalGet('admin/structure/myentity_type'); 165 | $this->clickLink('Delete'); 166 | $this->assertTitleContains('Are you sure you want to delete', 'Delete page title is correct'); 167 | $this->assertText('All entities of this type will also be deleted', 'Delete explanation is present'); 168 | $this->drupalPostForm(NULL, array(), t('Confirm')); 169 | $this->assertTitleContains('My entity subtypes', 'Went back to the right page'); 170 | $this->assertNoText('This is the description', 'Entity subtype is gone'); 171 | $this->drupalGet('myentity/1'); 172 | $this->assertResponse('404', 'After deleting subtype, page does not exist'); 173 | 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal8/mymodule/src/Tests/MyModulePageTest.php: -------------------------------------------------------------------------------- 1 | nodes[] = $this->drupalCreateNode(array( 38 | 'changed' => $start + $i, 39 | 'created' => $start + $i, 40 | )); 41 | } 42 | 43 | // Make sure local tasks and page title are showing. 44 | $this->drupalPlaceBlock('local_tasks_block'); 45 | $this->drupalPlaceBlock('page_title_block'); 46 | } 47 | 48 | /** 49 | * Tests the pages and blocks generated by the module. 50 | * 51 | * @see mymodule.routing.yml 52 | * @see mymodule.links.menu.yml 53 | * @see \Drupal\mymodule\Routing\MyModuleRouting 54 | * @see \Drupal\mymodule\Controller\MyUrlController 55 | * @see \Drupal\mymodule\Plugin\Block\MyModuleFirstBlock 56 | */ 57 | function testPages() { 58 | $page_title = 'My page title'; 59 | $dynamic_page_title = 'New page title'; 60 | $block_title = 'This is a test'; 61 | $text_in_block = array( 62 | 'General information goes here', 63 | 'Colors', 64 | 'Blue', 65 | 'Materials', 66 | 'Characteristic', 67 | 'Steel', 68 | 'Light', 69 | ); 70 | 71 | // Several pages should not allow anonymous access. Test this. 72 | $paths = array('mymodule/mypath', 'admin/people/mymodule', 'admin/content/mycontent/delete/5', 'mymodule/autocomplete'); 73 | foreach ($paths as $path) { 74 | $this->drupalGet($path); 75 | $this->assertResponse('403', "Access is denied to $path while not logged in"); 76 | } 77 | 78 | // Test the form while not logged in. It should be visible, but the 79 | // company field should not be visible. 80 | $this->drupalGet('mymodule/my_form_page'); 81 | $this->assertTitleContains('Personal data form', 'Form page title is correct'); 82 | $this->assertText('First name', 'Name field is shown'); 83 | $this->assertNoText('Company', 'Company field is not shown to anonymous user'); 84 | // Submit the form. 85 | $this->drupalPostForm(NULL, array('first_name' => 'Jennifer'), t('Submit')); 86 | $this->assertText('Thank you Jennifer', 'Submit message is shown'); 87 | 88 | // Test alteration of the user register form. 89 | $this->drupalGet('user/register'); 90 | $this->assertText('Company e-mail address', 'Email label has been altered'); 91 | $this->drupalPostForm(NULL, array('name' => 'foo', 'mail' => 'test@example.com'), t('Create new account')); 92 | $this->assertText('You are not allowed to register', 'Validation message displayed'); 93 | $this->assertNoText('Thank you for applying', 'Registration message not displayed'); 94 | 95 | // Log in. 96 | $account = $this->drupalCreateUser(array('administer mymodule', 'access administration pages', 'administer users', 'use company field', 'delete mycontent items')); 97 | $this->drupalLogin($account); 98 | 99 | // Check title and text on page in mymodule.routing.yml file. 100 | $this->drupalGet('mymodule/mypath'); 101 | $this->assertResponse('200', 'Access is allowed to page while logged in'); 102 | $this->assertTitleContains($page_title, 'Page title is shown'); 103 | for($i = 5; $i < 15; $i++) { 104 | $this->assertText($this->nodes[$i]->label(), "Node title $i appears on the page"); 105 | } 106 | for($i = 0; $i < 5; $i++) { 107 | $this->assertNoText($this->nodes[$i]->label(), "Node title $i does not appear on the page"); 108 | } 109 | 110 | // Go to second page of paged output and test. 111 | $this->drupalGet('mymodule/mypath', array('query' => array('page' => 1))); 112 | $this->assertTitleContains($page_title, 'Page title is correct'); 113 | for($i = 5; $i < 15; $i++) { 114 | $this->assertNoText($this->nodes[$i]->label(), "Node title $i does not appear on the page"); 115 | } 116 | for($i = 0; $i < 5; $i++) { 117 | $this->assertText($this->nodes[$i]->label(), "Node title $i appears on the page"); 118 | } 119 | 120 | // Verify that the link from mymodule.links.menu.yml works. 121 | $this->drupalGet('admin/structure'); 122 | $this->assertLink('Configure My Module', 0, 'Link is present on admin/structure'); 123 | $this->assertText('Longer description goes here', 'Description is present on admin/structure'); 124 | $this->clickLink('Configure My Module'); 125 | $this->assertTitleContains($page_title, 'Link click went to the right page'); 126 | for($i = 5; $i < 15; $i++) { 127 | $this->assertText($this->nodes[$i]->label(), "Node title $i appears on the page"); 128 | } 129 | for($i = 0; $i < 5; $i++) { 130 | $this->assertNoText($this->nodes[$i]->label(), "Node title $i does not appear on the page"); 131 | } 132 | 133 | // Check the MyModuleRouting class. 134 | // Normally the page title on admin/people is "People", but it's been 135 | // altered to be "User accounts". 136 | $this->drupalGet('admin/people'); 137 | $this->assertTitleContains('User accounts', 'Title was altered'); 138 | // Test the dynamic route. 139 | $this->drupalGet('admin/people/mymodule'); 140 | $this->assertTitleContains($dynamic_page_title, 'admin/people/mymodule has right title'); 141 | for($i = 5; $i < 15; $i++) { 142 | $this->assertText($this->nodes[$i]->label(), "Node title $i appears on the page"); 143 | } 144 | for($i = 0; $i < 5; $i++) { 145 | $this->assertNoText($this->nodes[$i]->label(), "Node title $i does not appear on the page"); 146 | } 147 | 148 | // Place the block and test it. 149 | $this->drupalPlaceBlock('mymodule_first_block', array( 150 | 'region' => 'content', 151 | 'label' => $block_title, 152 | )); 153 | $this->drupalGet('user'); 154 | $this->assertText($block_title, 'Block title is shown on page'); 155 | foreach ($text_in_block as $text) { 156 | $this->assertText($text, "Text $text appears on the page"); 157 | } 158 | 159 | // Test the form. 160 | $this->drupalGet('mymodule/my_form_page'); 161 | $this->assertTitleContains('Personal data form', 'Form page title is correct'); 162 | $this->assertText('First name', 'Name field is shown'); 163 | $this->assertText('Company', 'Company field is shown to user with permission'); 164 | // Submit the form. 165 | $this->drupalPostForm(NULL, array('first_name' => 'Jennifer', 'company' => 'Poplar'), t('Submit')); 166 | $this->assertText('Thank you Jennifer from Poplar', 'Submit message is shown'); 167 | // Test that JavaScript information is included. 168 | $this->assertRaw('mymodule.js', 'JavaScript file is referenced'); 169 | 170 | // Test the confirm delete form. 171 | $this->drupalGet('admin/content/mycontent/delete/5'); 172 | $this->assertText('Are you sure you want to delete content item 5?', 'Question is on the confirm form page'); 173 | // Try the cancel link. Should go to mymodule/mypath. 174 | $this->clickLink(t('Cancel')); 175 | $this->assertTitleContains($page_title, 'Cancel went to right place'); 176 | // Try submitting the form. Should go to mymodule/my_form_page. 177 | $this->drupalPostForm('admin/content/mycontent/delete/5', array(), t('Confirm')); 178 | $this->assertTitleContains('Personal data form', 'Form submit redirected to right place'); 179 | $this->assertText('Would have deleted 5', 'Submit message is displayed'); 180 | 181 | // Test the autocomplete path. 182 | $this->drupalGet('mymodule/autocomplete', array('query' => array('q' => 'test'))); 183 | $additions = array('add', 'choice', 'more', 'plus', 'something'); 184 | foreach ($additions as $word) { 185 | $this->assertText('test' . $word, "Autocomplete choice for $word is in the output"); 186 | } 187 | 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /code_2nd_ed/drupal8/mymodule/src/Tests/MyModuleSnippetsTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($cache_class, 'Cache class is not null'); 53 | $cache_class = $container->get('cache.' . $bin); 54 | 55 | $cache_class->set($cid, $data, CacheBackendInterface::CACHE_PERMANENT, $tags); 56 | 57 | // Check that we can retrieve data from the cache. 58 | $out = $cache_class->get($cid); 59 | $this->outputVariable($out, 'Cache get method output'); 60 | $this->assertEqual($data, $out->data, 'Cached data could be retrieved'); 61 | 62 | // Invalidate the data and check that it cannot be retrieved. 63 | Cache::invalidateTags($tags); 64 | $out = $cache_class->get($cid); 65 | $this->assertFalse($out, 'After invalidating tags, cached data cannot be retrieved'); 66 | 67 | // Theme snippets from section "Making Your Output Themeable" in Chapter 2. 68 | 69 | $build['hello'] = array( 70 | '#input1' => t('Hello World!'), 71 | '#theme' => 'mymodule_hookname', 72 | ); 73 | // This call to drupal_render_root() is just for testing purposes (to 74 | // verify that the theme snippet works). Calling this function directly is 75 | // definitely not recommended! 76 | $output = drupal_render_root($build); 77 | $expected = '' . print_r($var, TRUE), ''); 24 | } 25 | 26 | /** 27 | * Verbose output of HTML code with optional label. 28 | */ 29 | public function outputHTML($var, $label = '') { 30 | $this->verbose( 31 | (($label) ? '