from procedural to object-oriented php in drupal
TRANSCRIPT
From Procedural to
Object-Oriented PHP in
DrupalPresented by Amber Matz
PNWPHP, Seattle, WA September 11, 2015
Amber Matz@amberhimesmatz
Use #PNWPHP
Drupal trainer and developer
Web developer since 1999-ish
Not an astronaut
What This Session Is About
What Do I Mean By Procedural PHP?
• Code is organized into functions
• Functions are grouped into PHP files (.inc)
• Output is accessed through variables (usually strings or arrays)
Problems with Procedural Code
• To re-use code means to duplicate code into a new module, then make changes
• Code is hard to test when it’s tightly coupled
• Function loading order is magical and mysterious
• StdClass Obj provides only values, no hinting
What Is Object-Oriented PHP?
• Encapsulation provides centralized access to methods and values
• Inheritance of values and function made possible
• Interfaces set expectations
• OO-PHP allows for design patterns
• Easier to re-use, maintain, and extend
Before & After: Contact Module in D7 & D8
How It Works: D7’s Contact Module
D7 Contact Module Structure• drupalroot/
• modules/ • contact/
• contact.info • contact.module • contact.install • contact.admin.inc • contact.pages.inc • contact.test
D7: contact.info
• INI syntax (with Drupal additions)
• name, description, package, version
• extra files (the contact.test)
• path to configuration page
D7: contact.module• hook_help
• hook_permission
• hook_menu (admin UI pages, local tabs, form paths and callbacks)
• contact_load (loads a contact category)
• hook_mail
• hook_form_FORM_ID_alter (contact_form_user_profile_form_alter)
• alters the user profile form and adds a tab to the personal contact form
• hook_user_presave
• contact_form_user_admin_settings_alter
• adds default personal contact form settings to user settings page
D7: contact.admin.inc• Admin page callbacks for Contact module
• contact_category_list
• categories list, theme function
• contact_category_edit_form
• + validate and submit handlers
• contact_category_delete_form
• + confirm_form()
• + submit handler
D7: contact.pages.inc
• Form constructor + handlers for site-wide contact form
• Form constructor + handlers for personal contact form
D7: contact.install
• hook_schema
• db schema for data storage
• hook_uninstall
• various update hooks
D7: contact.test
• Tests for the Contact module
• Uses SimpleTest
• For tests that need to fully or partially bootstrap Drupal
• Uses OO-PHP
• class ContactSitewideTestCase extends DrupalWebTestCase
Why Use OO-PHP in Drupal 8?
• Testable code
• Better organization
• Decoupling reduces dependencies
• Interfaces & Data Types improve insight
How Is OO-PHP Done in Drupal 8?
• Much of the code base has been refactored to be object-oriented
• Each class is in its own file
• Classes must be namespaced
• External libraries introduced that Drupal extends
• Design pattern-based best practices in use
How It Works: D8’s Contact Module
D8: Contact Module Location
• drupalroot/
• core/
• modules/
• contact/
D8: Contact Module Base Files
• contact/
• contact.info.yml
• contact.module
.info Replaced by YAML filesD7: contact.info
name = Contactdescription = Enables the use of both personal and site-wide contact forms.package = Coreversion = VERSIONcore = 7.xfiles[] = contact.testconfigure = admin/structure/contact
D8: contact.info.yml
name: Contacttype: moduledescription: 'Enables the use of both personal and site-wide contact forms.'package: Core# version: VERSION# core: 8.xconfigure: entity.contact_form.collection
D8: Contact Module Files (cont’d)• contact/
• contact.routing.yml
• contact.links.action.yml
• contact.links.task.yml
• contact.links.menu.yml
• contact.permissions.yml
• contact.services.yml => Defines Services
• contact.views.inc
D8: Contact Module Directories• contact/
• config/
• migration_templates/
• src/
• tests/
D8: Contact Config Files• contact/
• config/
• install/
• contact.form.personal.yml
• contact.settings.yml
• contact/
• config/
• schema/
• contact.views.schema.yml
• contact.schema.yml
replaces hook_schema in contact.install
D8: Migration Templates
• contact/
• migration_templates/
• d6_contact_category.yml
• d6_contact_settings.yml
Drupal 6 to 8 Migrationhttps://www.drupal.org/node/2350521
PSR-4 Namespaces and Autoloading• Drupal 8 uses PSR-4 standard for PHP namespace autoloading
• Upgrading a module from D7 to D8 requires use of PSR-4
• Each module has a namespace that corresponds to its module name
• The module’s namespace is mapped to the src directory in the module’s directory.
• Anything after the namespace directly correlates to the directory and file structure in the src directory.
• The class name correlates to the file name with a php extension.
namespace Drupal\contact\Access
Namespace Mapping
namespace Drupal\contact
D8: Contact’s Classes• contact/
• src/
• ContactFormAccessControlHandler.php
• ContactFormEditForm.php
• ContactFormInterface.php
• ContactFormListBuilder.php
• ContactMessageAccessControlHandler.php
• MailHandler.php
• MailHandlerException.php
• MailHandlerInterface.php
• MessageForm.php
• MessageInterface.php
• MessageViewBuilder.php
PSR-4 namespaces & autoloading in Drupal 8
https://www.drupal.org/node/2156625
Upgrading contact.module
We still have hooks in D8
contact.module• Functions/Hooks added to accommodate changes in functionality or API
• i.e. hook_entity_extra_field_info_alter, which adds “pseudo-field" components on contact form
• Still contains some hooks/functions (i.e. hook_help), but…
• Values and parameters updated to use new D8 objects, interfaces, or architecture
function contact_help($route_name, RouteMatchInterface $route_match) { switch ($route_name) { case 'help.page.contact':
function contact_help($path, $arg) { switch ($path) { case 'admin/help#contact': vs
Menus & Routes
• Where is hook_menu?
• D7’s hook_menu replaced by new systems for routing, menu links, local tasks, actions, and contextual links https://www.drupal.org/node/1800686
• Oversimplification:
• Instead of hook_menu in .module, use .yml files
D7 contact_menu$items['contact'] = array( 'title' => 'Contact', 'page callback' => 'drupal_get_form', 'page arguments' => array('contact_site_form'), 'access arguments' => array('access site-wide contact form'), 'type' => MENU_SUGGESTED_ITEM, 'file' => 'contact.pages.inc', );
D8 contact.
routing.yml
contact.site_page: path: '/contact' defaults: _title: 'Contact' _controller: '\Drupal\contact\Controller\ContactController::contactSitePage' contact_form: NULL requirements: _permission: 'access site-wide contact form'
Routing system in Drupal 8https://www.drupal.org/developing/api/8/routing
D7 to D8 upgrade tutorial: Convert hook_menu() and
hook_menu_alter() to Drupal 8 APIshttps://www.drupal.org/node/2118147
hook_form_FORM_ID_alterD7: contact.module
/** * Implements hook_form_FORM_ID_alter(). * * Add the default personal contact setting on the user settings page. * * @see user_admin_settings() */function contact_form_user_admin_settings_alter(&$form, &$form_state)
hook_form_FORM_ID_alterD8: contact.module
/** * Implements hook_form_FORM_ID_alter(). * * Add the default personal contact setting on the user settings page. * * @see \Drupal\user\AccountSettingsForm */function contact_form_user_admin_settings_alter(&$form, FormStateInterface $form_state)
@see \Drupal\user\AccountSettingsForm
• Get form ID from method getFormID()*
public function getFormId() { return 'user_admin_settings'; }
* sometimes this isn’t so straightforward
Permissions
• D7: Permissions defined in hook_permission in contact.module
• D8: Permissions defined in contact.permissions.yml
• Change record: https://www.drupal.org/node/2311427
D7 contact_permissonfunction contact_permission() { return array( 'administer contact forms' => array( 'title' => t('Administer contact forms and contact form settings'), ), 'access site-wide contact form' => array( 'title' => t('Use the site-wide contact form'), ), 'access user contact forms' => array( 'title' => t("Use users' personal contact forms"), ), );}
D8 contact.
permissions. yml
administer contact forms: title: 'Administer contact forms and contact form settings'access site-wide contact form: title: 'Use the site-wide contact form'access user contact forms: title: 'Use users'' personal contact forms'
Access Control
Access to Personal Contact FormD7
contact.module
_contact_personal_tab_access
• defines access control to a user’s personal contact form
D8 src/Access/ContactPageAccess.php
class ContactPageAccess
• defines access control to a user’s personal contact form
Complex Access Control as a Service
• A more complex access check is used for the personal contact form (beyond just checking for a permission)
• Complex access control must be registered as a service
• Benefit: greater testing flexibility because you can mock the service
Service: Personal Tab AccessIn contact.services.yml:
services: access_check.contact_personal: class: Drupal\contact\Access\ContactPageAccess tags: - { name: access_check, applies_to: _access_contact_personal_tab } arguments: ['@config.factory', '@user.data']
Access Checking on Routeshttps://www.drupal.org/node/2122195
Services and dependency injection in Drupal 8
https://www.drupal.org/node/2133171
D7: Message Building• Implements hook_mail in contact.module
• Builds email message
• Sets site name, subject, (optional) category, message, sender name, sender email
• Builds auto-reply message
• Builds “send copy to myself” message
D8: Message Building• Defines Message entity in src/Entity/Message.php
• class Message extends ContentEntityBase implements MessageInterface
• src/MessageInterface.php
• Builds message form, workflow, and actions in src/MessageForm.php
• class MessageForm extends ContentEntityForm
• Builds and formats the message into a format suitable for email in src/MessageViewBuilder.php
D8: Mail Handling
• MailHandler, MailHandlerException, MailHandlerInterface
• handles assembly and dispatch of contact mail messages
• Drupal\contact\MailHandler is registered as a service in contact.services.yml
New in D8: MailHandler Unit Test
• The Drupal\Contact\MailHandler has its own unit test!
• Location: tests/src/Unit/MailHandlerTest.php
• Uses PHPUnit
MailHandler _construct()
public function __construct( MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, LoggerInterface $logger, TranslationInterface $string_translation, EntityManagerInterface $entity_manager)
MailHandlerTest Setup$this->mailManager = $this->getMock('\Drupal\Core\Mail\MailManagerInterface'); $this->languageManager = $this->getMock('\Drupal\Core\Language\LanguageManagerInterface'); $this->logger = $this->getMock('\Psr\Log\LoggerInterface'); $this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface');$this->userStorage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
Testing in Drupal 8
• SimpleTest is joined by PHPUnit testing framework
• Use PHPUnit for unit tests
• SimpleTest should only be used when a full or partial bootstrap of Drupal is necessary
D8FTW: Unit Testing For Realsies
by Larry Garfield
https://www.palantir.net/blog/d8ftw-unit-testing-realsies
An article to read about unit testing in Drupal 8:
D8: Contact Module Tests
• SimpleTest Tests:
• src/Tests
• PHPUnit Tests
• tests
Upgrading & Refactoring contact.admin.inc
admin.inc refactor
• Functionality changed to utilize D8 Entities and Fields
• Instead of add categories, add form
• Instead of configuring recipients per category, set per form
• Instead of only adding categories, add fields of any type
D7• contact.admin.inc
• Procedural functions:
• Categories: get from db and list in a table (theme)
• Form: edit categories
D8• src/ContactFormInterface.php
— interface for defining a contact form entity
• src/ContactFormEditForm.php — base form class for contact form edit forms
• src/ContactFormListBuilder.php— class that builds a listing of contact forms
• src/Entity/ContactForm.php— class that defines the contact form entity (implements the ContactFormInterface)
Admin refactoring
Changes in Form API
Before: D7 Form API
• Magically named functions
• Form ID = name of the function that builds and return $form array
• Validate and submit handlers should exist…but no enforcement except for hitting your head against the wall
Before: D7 Form FunctionsFunction: Build and return the form (function name = Form ID)
• function contact_category_edit_form($form, &$form_state, array $category = array())
Function: Validate the form
• function contact_category_edit_form_validate($form, &$form_state)
Function: Submit the form
• function contact_category_edit_form_submit($form, &$form_state)
After: D8 Form MethodsName and Return the Form IDpublic function getFormID()
Build the Formpublic function buildForm(array $form, FormStateInterface $form_state)
Validate the Formpublic function validateForm(array &$form, FormStateInterface $form_state)
Submit the Formpublic function submitForm(array &$form, FormStateInterface $form_state)
FormInterfacenamespace Drupal\Core\Form;
interface FormInterface {
public function getFormId();
public function buildForm(array $form, FormStateInterface $form_state);
public function validateForm(array &$form, FormStateInterface $form_state);
public function submitForm(array &$form, FormStateInterface $form_state);
}
D8 Forms: Extend & Use Interface
• Choose a class or “inheritance tree” that closely matches your use-case and extend it!
• For basic forms, extend FormBase and use the FormInterface to learn about which methods to use
• $form_state uses FormStateInterface
• See the FormInterface for parameter hints!
One more thing about contact.admin.inc…
Delete Handling: “Abstracted Out”
function contact_category_delete_form ($form, &$form_state, array $contact) { $form['contact'] = array( '#type' => 'value', '#value' => $contact, ); return confirm_form( $form, t('Are you sure you want to delete %category?', array('%category' => $contact['category'])), 'admin/structure/contact', t('This action cannot be undone.'), t('Delete'), t('Cancel') );} function contact_category_delete_form_submit ($form, &$form_state)…
/** * Defines the contact form entity. * * @ConfigEntityType( * id = "contact_form", * label = @Translation("Contact form"), * handlers = { * "access" = "Drupal\contact\ContactFormAccessControlHandler", * "list_builder" = "Drupal\contact\ContactFormListBuilder", * "form" = { * "add" = "Drupal\contact\ContactFormEditForm", * "edit" = "Drupal\contact\ContactFormEditForm", * "delete" = "Drupal\Core\Entity\EntityDeleteForm" * } * },
Annotation in src/Entity/ContactForm.phpProcedural functions in contact.admin.inc
`
That was a plugin annotation
An Overview of the Drupal 8 Plugin System
by Joe Shindelar
https://www.youtube.com/watch?v=gd6s4wC_bP4
DrupalCon Los Angeles 2015
pages.inc refactor
• contact.pages.inc replaced by src/Controller/ContactController.php
• Refactored to use Controller class (extends ControllerBase)
• Uses new menu and routing system for paths and menu items
Got module.pages.inc? Extend ControllerBase
D7: Displaying Site-Wide Contact Form on a Page
contact.module contact_menu
page callback = ‘drupal_get_form’
page argument = ‘contact_site_form’
file = ‘contact.pages.inc’
contact.pages.incfunction
contact_site_form$items[‘contact’] (where ‘contact’ = the path)
/contact
D8: Displaying Site-Wide Contact Form on a Page
contact.routing.yml contact.site_page:
path: ‘/contact’
_controller: ‘\Drupal\contact \Controller \ContactController ::contactSitePage'
contact_form: NULL
src/Controller/ ContactController.php
class ContactController extends ControllerBase
public function ContactSitePage(
ContactFormInterface $contact_form = NULL)
State of OO-PHP in Contact Module
• Much of the code base has been refactored to be object-oriented
• Each class is in its own file
• Classes are namespaced
• Unit tests introduced
• Classes extend others, use interfaces
:wipes brow:
What Do I Need to Know?
• Language features of OO-PHP, such as Constructors, Inheritance, Interfaces, Namespaces, and Type Hinting
• Design patterns and conventions such as Dependency Injection, Services Container, PSR-4
• Other Drupal 8-isms such as Plugins and Annotations
Porting Drupal 7 Modules
• You could try an automated solution to help you learn about changes in the API
• Drupal Module Upgraderhttps://www.drupal.org/project/drupalmoduleupgrader
https://www.drupal.org/project/drupalmoduleupgrader
D8 Code Generator/Scaffolding
• Generate boilerplate code and scaffolding to jump-start refactoring
• Drupal Consolehttp://drupalconsole.com/
D8 Module Developer Resources• api.drupal.org
• Handbook docs on drupal.org
• Change recordsdrupal.org/list-changes
• Blog posts
• DrupalCon Session Recordingshttps://www.youtube.com/user/DrupalAssociation/playlists
• Symfony2 Docs
• Training companies
Disclaimer: All sources contain outdated info, may or may not be accurate, and will undoubtedly be replaced by updated/new documentation once release candidate and stable versions are released.
Happy D8 Developing!
@amberhimesmatz
Amber Matz