Drupal 8

  • Drupal 8: Change Commerce Product Variation URL to SKU

    To pull this off, we need to write a custom module. Let's name our module commerce_variation_sku. In our commerce_variation_sku.module file, we need implement the hook hook_entity_type_build to change the entity class and entity storage class to our custom classes. /** * Implements hook_entity_type_build(). */ function commerce_variation_sku_entity_type_build(array &$entity_types) { if (isset($entity_types['commerce_product_variation'])) { $entity_types['commerce_product_variation']->setClass('Drupal\commerce_variation_sku\Entity\ProductVariation'); $entity_types['commerce_product_variation']->setStorageClass('Drupal\commerce_variation_sku\ProductVariationStorage'); } } Next, we create our custom entity class that will extend the original entity class. <?php namespace Drupal\commerce_variation_sku\Entity; use Drupal\Core\Url; use Drupal\commerce_product\Entity\ProductVariation as ProductVariationBase; class ProductVariation extends ProductVariationBase { /** * {@inheritdoc} */ public function toUrl($rel = 'canonical', array $options = []) { // Product variation URLs depend on the parent product. if (!$this->getProductId()) { // RouteNotFoundException tells EntityBase::uriRelationships() // to skip this product variation's link relationships. throw new RouteNotFoundException(); } // StringFormatter assumes 'revision' is always a valid link template. if (in_array($rel, ['canonical', 'revision'])) { $route_name = 'entity.commerce_product.canonical'; $route_parameters = [ 'commerce_product' => $this->getProductId(), ]; $options += [ 'query' => [ 'sku' => $this->getSku(), ], 'entity_type' => 'commerce_product', 'entity' => $this->getProduct(), // Display links by default based on the current language. 'language' => $this->language(), ]; return new Url($route_name, $route_parameters, $options); } else { return parent::toUrl($rel, $options); } } }   Then we create our custom entity storage class that again extends the original entity storage class. <?php namespace Drupal\commerce_variation_sku; use Drupal\commerce_product\Entity\ProductInterface; use Drupal\commerce_product\ProductVariationStorage as ProductVariationStorageBase; /** * Defines the product variation storage. */ class ProductVariationStorage extends ProductVariationStorageBase { /** * {@inheritdoc} */ public function loadFromContext(ProductInterface $product) { $current_request = $this->requestStack->getCurrentRequest(); if ($sku = $current_request->query->get('sku')) { $variation = $this->loadBySku($sku); if (in_array($variation->id(), $product->getVariationIds())) { /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ if ($variation->isPublished() && $variation->access('view')) { return $variation; } } } return $product->getDefaultVariation(); } } Last step is to clear your cache and you're good to go ūüėÄ 
  • Automated testing with Drupal 8 + Selenium + Behat + Gherkins + Jenkins + Cucumber

    Here's a simple guide on how to setup automated testing with Behat, Behat Gherkins, Selenium, Jenkins and Cucumber with Drupal 8/9. In this guide, we will be using a Drupal 8 instance with composer. Setup Behat for Drupal 8/9 First thing we need to do is to install all the composer libraries that we need by installing drupal/drupal-extension. composer require drupal/drupal-extension --dev Creating a file called behat.yml inside the root directory of your project  default: suites: default: contexts: - FeatureContext - Drupal\DrupalExtension\Context\DrupalContext - Drupal\DrupalExtension\Context\MinkContext extensions: Behat\MinkExtension: goutte: ~ base_url: https://www.mysite.com javascript_session: selenium2 browser_name: 'chrome' selenium2: wd_host: http://localhost:4444/wd/hub capabilities: { "browser": "chrome", "version": "*", 'chrome': {'switches':['--start-maximized']}} Drupal\DrupalExtension: blackbox: ~ api_driver: drupal drupal: drupal_root: web/ region_map: navigation: ".navbar-header" navigation_collapsible: "#navbar-collapse" header: ".region-header" highlighted: ".highlighted" help: ".region-help" content: ".main-content" sidebar_first: ".region-sidebar-first" sidebar_second: ".region-sidebar-second" footer: ".footer" local: extensions: Drupal\MinkExtension: base_url: http://mysite.localhost:8000 Initialize behat. This creates the features folder where we will put all our features files and some of our custom context if we needed. Custom context will be inside the features/bootstrap directory. behat --init +d features/bootstrap - place your context classes here +f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here To check if everything is working as expected, you can try listing definitions by: behat -dl You should see something like this: default | Given I am an anonymous user default | Given I am not logged in default | Then I log out default | Given I am logged in as a user with the :role role(s) default | Given I am logged in as a/an :role default | Given I am logged in as a user with the :role role(s) and I have the following fields: default | Given I am logged in as :name default | Given I am logged in as a user with the :permissions permission(s) default | Then I should see (the text ):text in the :rowText row default | Then I should not see (the text ):text in the :rowText row default | Given I click :link in the :rowText row default | Then I (should )see the :link in the :rowText row default | Given the cache has been cleared default | Given I run cron ...   Setup Selenium server with Chrome driver On this part, we will be using a standalone Selenium server. Create a directory that you can easily access. E.g.: mkdir ~/selenium-server Download the standalone Selenium server  and move it in to the folder you just created. Download your desired browser to use for testing. In this guide, we will be using Chrome driver. Head to https://chromedriver.chromium.org/ to download the right Chrome driver version that fits your Chrome browser version.  Extract the zip file and move the chromedriver executable file in to the same folder we created. Run the server cd ~/selenium-server java -Dwebdriver.chrome.driver=./chromedriver -jar selenium-server-standalone-3.141.59.jar     Create feature and run the test Create a new file inside features folder and name it like homepage.feature @api @javascript Feature: Homepage check Scenario: Anonymous user access Given I am an anonymous user When I visit "/" Then I should see the text "Featured" in the "content" region This example feature file will access the homepage as an anonymous user and will look for the text "Featured" in the "content" region (which the css selector were specified in the behat.yml)   Now let's run the test by executing: behat --profile local I'm running the local profile (which we also specified the url from our behat.yml) since I'm running the test from my local.  
  • Drupal 8/9 Social Auth Buttons

    Finally an easy way to render Social Auth links/buttons. Simply download and enable Social Auth Buttons and the buttons will be rendered in Drupal's default login form. You can even embed and render it to your custom forms like this:   $form['social_auto_buttons'] = [ '#type' => 'social_auth_buttons', '#title' => t('Social Auth Buttons'), ];   Theming The form elements that came with this module are fully themeable. Below is a sample to add icons to the buttons using Font Awesome icons Make sure you have Font Awesome libraries loaded in to your theme. [theme].libraries.yml ... fontawesome: version: VERSION js: node_modules/@fortawesome/fontawesome-free/js/all.min.js: {} css: component: node_modules/@fortawesome/fontawesome-free/css/all.min.css: {} ... Theme the buttons using hook_preprocess_social_auth_buttons_link() [theme].theme   ... /** * Implements hook_preprocess_HOOK(). */ function [theme]_preprocess_social_auth_buttons_link(&$variables) { /* Your code here */ $icons = [ 'facebook' => 'fab fa-facebook-f', 'google' => 'fab fa-google', 'instagram' => 'fab fa-instagram', ]; $id = $variables['name']; if (isset($icons[$id])) { $variables['icon'] = [ '#markup' => '<i class="' . $icons[$id] . '"></i>', ]; } $variables['attributes']['class'][] = 'btn-' . $id; } ...  
  • Get active theme in Drupal 8

    if you want to get actual active theme name (administration theme included), use:  $activeTheme = \Drupal::service('theme.manager')->getActiveTheme(); if you want your default front end theme, use: $defaultThemeName = \Drupal::config('system.theme')->get('default');  
  • Drupal 8 site with Docker + Solr search (docker4drupal by wodby)

    This tutorial applies to docker4drupal by wodby. PART 1: Install and configure Drupal 8 Search API Solr module Install Search API Solr module. After installing the module, configure Solr server. Go to Home > Administration > Configuration > Search and metadata > Search API Create a new server or edit an existing one and update the fields under CONFIGURE SOLR BACKEND field group HTTP protocol: http Solr host: solr Solr port: 8983 Solr path: [empty] Solr core: [NAME OF YOUR CORE THAT WE WILL USE ON PART 2]   PART 2: Creating Solr core SSH to a runnig Solr container: make shell solr or docker-compose exec solr sh Create Solr core make create core=[core name that we want to use on PART 1] -f /usr/local/bin/actions.mk  
  • Drupal 8 Docker with multisite setup

    1. Setup Docker Download latest release of Docker4Drupal and install as Vanilla Drupal or to an existing codebase. Follow the comprehensive tutorial from https://wodby.com/docs/1.0/stacks/drupal/local/#usage   2. Update Docker configurations Set the default site domain name in .env  ... PROJECT_BASE_URL=mycoolsite.localhost ...   Create multiple instances of database servers in docker-compose.yml ... default: image: wodby/mariadb:$MARIADB_TAG container_name: "${PROJECT_NAME}_default" stop_grace_period: 30s environment: MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD MYSQL_DATABASE: $DB_NAME MYSQL_USER: $DB_USER MYSQL_PASSWORD: $DB_PASSWORD ports: - 42330:3306 # Port that we can access outside of the container volumes: - ./assets/default.sql:/docker-entrypoint-initdb.d/dump.sql # Place init .sql file(s) here. subsite: image: wodby/mariadb:$MARIADB_TAG container_name: "${PROJECT_NAME}_subsite" stop_grace_period: 30s environment: MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD MYSQL_DATABASE: $DB_NAME MYSQL_USER: $DB_USER MYSQL_PASSWORD: $DB_PASSWORD ports: - 42331:3306 # Port that we can access outside of the container volumes: - ./assets/subsite.sql:/docker-entrypoint-initdb.d/dump.sql # Place init .sql file(s) here. ...   Set web server (nginx) configurations ... nginx: image: wodby/nginx:$NGINX_TAG container_name: "${PROJECT_NAME}_nginx" depends_on: - php environment: NGINX_STATIC_OPEN_FILE_CACHE: "off" NGINX_ERROR_LOG_LEVEL: debug NGINX_BACKEND_HOST: php NGINX_SERVER_ROOT: /var/www/html/web NGINX_VHOST_PRESET: $NGINX_VHOST_PRESET # NGINX_DRUPAL_FILE_PROXY_URL: http://example.com volumes: - ./:/var/www/html:cached ports: - "8001:80" # port to use for proxy labels: - 'traefik.backend=${PROJECT_NAME}_nginx' - 'traefik.frontend.rule=HostRegexp:{subdomain:[a-z]+}.${PROJECT_BASE_URL}' # subdomain ...   3. Drupal site configurations In this tutorial we will have these 2 sites: Default Name: default Domain: mycoolsite.localhost Port: 80001 Subsite Name: subsite Domain: subsite.mycoolsite.localhost Port: 80001     First thing is we need to make changes on sites.php file base on the site informations above.  ... $sites['8001.mycoolsite.localhost'] = 'default'; // Docker default site $sites['8001.subsite.mycoolsite.localhost'] = 'subsite'; // Docker subsite site ...   Next is we need to make sure we have these 2 folders under the sites folder: default subsite   Add the database configurations for both sites using the hostname that we configured from docker-compose.yml.   sites/default/settings.php (default) ... $databases['default']['default'] = [ 'database' => 'drupal', 'username' => 'drupal', 'password' => 'drupal', 'prefix' => '', 'host' => 'default', 'port' => '3306', 'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql', 'driver' => 'mysql', ]; ...   sites/subsite/settings.php (default) ... $databases['default']['default'] = [ 'database' => 'drupal', 'username' => 'drupal', 'password' => 'drupal', 'prefix' => '', 'host' => 'subsite', 'port' => '3306', 'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql', 'driver' => 'mysql', ]; ...    
  • Drupal 8: Set entity field cardinality programatically inside a custom form

    This is how you can dynamically change a multi select entity field maximum number of selected items programmatically. //The sample codes below will get an entity form and render the entity form fields to a custom form. // Create the entity $entity = $this->orderItemStorage->create([ 'title' => 'Order Item', 'type' => 'bee', 'purchased_entity' => 'my_product_variation_id', 'quantity' => 1, 'unit_price' => '100', ]); // Add the entity to form_state $form_state->set('order_item', $entity); // Get the form display $form_display = $this->entityTypeManager->getStorage('entity_form_display')->load('commerce_order_item.bee.add_to_cart'); $form_state->set('order_item_form_display', $form_display); // Set the form parents to empty to avoid form rendering errors $form['#parents'] = []; // loop through the entity form fields foreach ($form_display->getComponents() as $name => $component) { $widget = $form_display->getRenderer($name); if (!$widget) { continue; } $items = $entity->get($name); if ($name === 'my_multiple_select_field') { // Get the field definition and field storage definition object and set the cardinality $definition = $items->getFieldDefinition(); $fieldStorageDefinition = $definition->getFieldStorageDefinition(); $fieldStorageDefinition->setCardinality(1); } $items->filterEmptyItems(); // Add the entity form field to your custom form $form[$name] = $widget->form($items, $form, $form_state); $form[$name]['#access'] = $items->access('edit'); }  
  • Drupal 8: Show Drupal throbber on button that manually calls Views AJAX RefreshView

    In this tutorial, I will guide you how to show Drupal's progress/throbber icon on your button that call the Views AJAX RefreshView.   Views Configuration Make sure your view is using AJAX. To do that, set the "Advanced > Use Ajax" on your view.   HTML Button <a href="#" class="btn btn-default reload-view-button">Refresh View</a>   Javascript Drupal.behaviors.refreshLink = { attach: function(context, settings) { if ($('.reload-view-button', context).length > 0) { $('.reload-view-button', context).once().on('click', function(e) { e.preventDefault(); var self = this; // Change [YOUR VIEW ID] to your actual view id // This loop will get the view instance from the view class var viewInstance = Object.values(Drupal.views.instances).find(function(item) { if (item.$view.length > 0 && $('.view-id-[YOUR VIEW ID]').length > 0) { return item.$view[0] === $('.view-id-[YOUR VIEW ID]')[0]; } }); if (viewInstance) { // Get the ajax throbber from theme var progressElement = $(Drupal.theme('ajaxProgressThrobber')); // Append the throbber to the button $(self).append(progressElement); // Refresh the view by ajax $('.view-id-[YOUR VIEW ID]').trigger('RefreshView'); // Override the success callback method viewInstance.refreshViewAjax.success = function(response, status) { // Call the original callback Drupal.Ajax.prototype.success.call(this, response, status); // ADD ALL THE THINGS WE WANT TO DO AFTER REFRESH VIEW AJAX SUCCESS! // Remove the throbber element that we added above $(progressElement).remove(); }; } }); } } }  
  • Drupal 8: Redirect to page from Event Subscriber with destination query

    Create your Event Subscriber Create your module's services yml file and declare the event subscriber services: mymodule.autologin: class: Drupal\mymodule\EventSubscriber\AutoLogin tags: - {name: event_subscriber} Save this as mymodule.services.yml inside mymodule folder Create the actual class used from above code <?php namespace Drupal\mymodule\EventSubscriber; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class AutoLogin implements EventSubscriberInterface { public function redirectToLogin() { // PUT YOUR LOGIC HERE } /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events[KernelEvents::REQUEST][] = ['redirectToLogin']; return $events; } } Save this as AutoLogin.php inside mymodule/src/EventSubscriber folder   Redirect using URL::fromRoute with destination query $current_path = \Drupal::service('path.current')->getPath(); $url = \Drupal\Core\Url::fromRoute('simplesamlphp_auth.saml_login', [], ['query' => ['destination' => $current_path, 'absolute' => TRUE]]); $response = new RedirectResponse($url->toString(), 302); $event->setResponse($response); Put the above code in the AutoLogin::redirectLogin(). This code will get the current path and redirect to saml login page to auto login, then if the login is successful, it will redirect to the original path that we are trying to access.  
  • Drupal 8 module: Bootstrap 4 Modal

    Are you using Drupal 8 Bootstrap 4 Theme (Barrio) and you want to use Drupal Ajax Dialog? Chances are you might have to do some extra coding just to make that Drupal core modal dialog make it look and feel like a bootstrap modal. Bootstrap 4 Modal module adds new type of dialog that you can use. Example of Drupal core ajax modal dialog: <a class="use-ajax" data-dialog-options="{&quot;width&quot;:400}" data-dialog-type="modal" href="/node/1"> First node displayed in core modal dialog. </a> This link will load drupal node id 1 by ajax on drupal core modal dialog with 400px width   Example of Bootstrap 4 Modal dialog: <a class="use-ajax" data-dialog-options="{&quot;dialogClasses&quot;:&quot;modal-dialog-centered&quot;,&quot;dialogShowHeader&quot;:false}" data-dialog-type="bootstrap4_modal" href="/node/1"> First node displayed in bootstrap 4 modal dialog. </a> This link will load drupal node id 1 by ajax on drupal core modal dialog with modal dialog vertically centered and no modal header.