Wednesday 28 June 2023

Modify a field config and storage with existing data in Drupal

 As a developer, we have faced such situations where we have to change the type of field in content type. If that field is already having data against the field then this is not possible through Drupal interface. And we cannot delete and create that field again as there will be data loss.

To meet this requirement, the proper way is to write hook_update

In my one of account, I faced such scenario where anyhow initially field was created as type "List(Text)" then we have to change it to "Boolean". We have already created a lot of contents on Production site. We don't want to update the all contents again after recreating this field. So I followed the hook_update to change that field config and storage with existing data.

I have created a custom module named mymodule and write down .install file to add hook_update. Code is as per below.


<?php

/**
 * @file
 * Update function to change field with existing data.
 */

use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;

/**
 * Recreate field_content_access storage so that "drush cim" can run.
 */
function connect_access_update_9001() {
  $fields = [
    'field_content_access' => [
      'table' => 'node__field_content_access',
      'revision_table' => 'node_revision__field_content_access',
    ],
  ];

  $database = \Drupal::database();

  foreach ($fields as $field_name => $f) {
    // Table name from $fields array.
    $table = $f['table'];
    // Revision table from $field array.
    $revision_table = $f['revision_table'];
    // Entity type like node.
    $entity_type = 'node';

    // Step 1.
// Get field storage.
    $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);

    // Then check if field is not found.
    if (is_null($field_storage)) {
      continue;
    }

    // Step 2.
// Store existing data in $rows and $revision_rows variables.
    $rows = NULL;
    $revision_rows = NULL;
    if ($database->schema()->tableExists($table)) {
      // The table data will be restored from $rows and $revision_rows variable
// after the update is completed.
      $rows = $database->select($table, 'n')->fields('n')->execute()
        ->fetchAll();
      $revision_rows = $database->select($revision_table, 'n')->fields('n')->execute()
        ->fetchAll();
    }

    // Step 3.
// Save new field configs in new variable and delete existing fields.
    $new_fields = [];
    // Traverse every field to assign those values to new field.
    foreach ($field_storage->getBundles() as $bundle => $label) {
      $field = FieldConfig::loadByName($entity_type, $bundle, $field_name);
      $new_field = $field->toArray();
      $new_field['field_type'] = 'boolean';
      $new_fields[] = $new_field;
      // Delete existing field.
      $field->delete();
    }

    // Step 4.
// Create new storage configs from existing configs.
    $new_field_storage = $field_storage->toArray();
    $new_field_storage['type'] = 'boolean';
    $new_field_storage['module'] = 'core';
    $new_field_storage['settings'] = [];

    // Step 5.
// Purge deleted fields data.
// Required to create new fields.
    field_purge_batch(200);

    // Step 6.
// Create new fieldstorage.
    FieldStorageConfig::create($new_field_storage)->save();

    // Step 7.
// Now create new fields for all bundles.
    foreach ($new_fields as $new_field) {
      $new_field = FieldConfig::create($new_field);
      $new_field->save();
    }

    // Step 8.
// Restore existing data in created fields & revision tables.
    if (!is_null($rows)) {
      foreach ($rows as $row) {
        $row = (array) $row;
        $database->insert($table)->fields($row)->execute();
      }
    }
    if (!is_null($revision_rows)) {
      foreach ($revision_rows as $row) {
        $row = (array) $row;
        $database->insert($revision_table)->fields($row)->execute();
      }
    }
  }
}

Follow below steps. 1. Machine name of the field must be same to new field. 2. On `admin/reports/fields` and search field to be changed. List down all the content types using this field. 3. Check table in the DB related with this field and check revision tables also. List down all the records here. Check with below queries either run by drush or from phpmyadmin. drush sql:query "SELECT * FROM node__field_content_access;" drush sql:query "SELECT * FROM node_revision__field_content_access;" 4. Add the above code in mymodule.install. 5. Replace the $entityType and $fieldName variables according to the entity and field have to change. 6. Take DB backup. 7. Run update either by drush updb or by /update.php on your browser. 8. Check if the update was successfully performed. 9. Check field in all content type. 10. Check data in existing content type.

Tuesday 22 November 2022

Install Drush Globally

 To install Drush globally follow below steps.

1. Run below command, To install Drush 8.x required for Drupal 8+.

    composer global require drush/drush:dev-master

2. Now run below command to add Drush to your system path.

         export PATH="$HOME/.composer/vendor/bin:$PATH"

3. Run below command to update to a newer version.

         composer global update

To install specific version of Drush, run below command.

        composer global require drush/drush:6.1.0

Some of the below common used Drush commands.

drush cc or cache:clear => To clear cache. When we run this command, will give some options to execute as below

          [0] Cancel

    [1] drush

    [2] theme-registry

    [3] router

    [4] css-js

    [5] render

    [6] plugin

    [7] bin

    [8] views

drush cr or cache:rebuild => Rebuild a Drupal 8 site.
drush updb or updatedb => Apply any database updates required (as with running update.php)
drush updbst or updatedb:status => List any pending database updates.
drush uli or user:login => Display a one time login link for user ID 1, or another user.
drush pml => To list modules
drush en module_name => To enable module
drush pmu => To uninstall module
drush site:install => To install Drupal site with Drush command.
drush sqlc or sql:cli => Open a SQL command-line interface using Drupal's credentials.
drush sql:dump => Exports the Drupal DB as SQL using mysqldump or equivalent.
drush sqlq or sql:query => Execute a query against a database.


Create Drupal 9 Project with Composer

 For Drupal 9, We should use "drupal/recommended-project" composer template. This will ensure that Drupal core dependencies are same as the official Drupal release.

Composer command is as below to create Drupal 9 project

composer create-project drupal/recommended-project first_drupal9_site

This command will create a project in "first_drupal9_site" and automatically will execute composer install to download latest stable version of Drupal and all its dependencies.

Now first_drupal9_site directory will have files as below.

1. composer.json (file)

2. composer.lock (file)

3. vendor (Directory)

4. web (Directory)

The web root will be "web" directory. Except "web" directory, all other files "composer.json, composer.lock, vendor" will not accessible to the web server. 


To install Drupal 9 site, now access your localhost and access web directory. Follow the steps as per installation asks. Once these steps will finish, your first Drupal 9 site will ready.


To install specific version, run below composer command.

composer create-project drupal/recommended-project:9.4.8 first_drupal9_site


To do modified installation

Before composer install execution, if I need to modify some of the properties of the downloaded composer.json then we have to use --no-install flag while running composer create-project

Example, if I want to rename the subdirectory "web" to something else. Follow below steps

  • Run command composer create-project --no-install drupal/recommended-project first_drupal_site 
  • Change directories to first_drupal_site and edit the composer.json file to suit your needs.

    For example, to change the sub-directory from 'web' to something else, the keys to modify are the 'extra' sub-keys 'web-root' (under 'drupal-scaffold') and 'installer-paths'.

  • Run composer install to download Drupal and all its dependencies. 
To download contributed module and contributed theme with composer follow below steps.
  • Run composer require drupal/module_name. eg. composer require drupal/token
  • This needs to be executed at the root of your Drupal install but not at the same level as the core directory.
Composer will then automatically update your composer.json, adding the module to all the other requirements in the list, like this:

{
    "require": {
        "drupal/token": "^1.5"
    }
}
Composer will download the module and all the possible dependencies it may have.

Specifying a version
You can specify the version of the module / theme you want to download as follows:

composer require 'drupal/module_name:version'
(...where version is replaced with a Composer version or version constraint.)

For example:
composer require 'drupal/token:^1.5' 
composer require 'drupal/simple_fb_connect:~3.0' 
composer require 'drupal/ctools:3.0.0-alpha26' 
composer require 'drupal/token:1.x-dev' 

To avoid problems on different terminals/shells, surround the version in quotes as in the examples above.

Note!: On Windows, single quotes actually might break the version specification and lead to failure to install with 'Could not parse version constraint .....': Invalid version string "....'".

Without the quotes, e.g.:
composer require drupal/ctools:^3.7 or with double quotes instead of single quotes, e.g.:
composer require "drupal/ctools:^3.7" works well.

Reference link Drupal.org

Monday 21 November 2022

Enable PHP OPCache in Xampp

To install Drupal 8 and upper version, PHP OPCache extension is required. Untill we will not enable this, Drupal 8+ versions can not be installed.

To enable this extension, Please follow below steps.

1. Open php.ini file which is configuration file for PHP.

2. Search for "Dynamic Extensions" keyword.

3. Under this heading, Either search for ";zend_extension=opcache" and uncomment this line by removing ";" or add new line by adding below code

   zend_extension="C:\xampp\php\ext\php_opcache.dll"

4. Make sure the "zend_extension" should have the exact path to the opcache.dll file.

5. opcache.dll file exists in xampp > php > ext directory.

6. Save the php.ini file.

7. Restart the apache service.

8. Now try to install Drupal 8+, issue of PHP OPCache fixed.


To get good performance, following settings for PHP OPCache are recommended.

opcache.memory_consumption=128

opcache.interned_strings_buffer=8

opcache.max_accelerated_files=4000

opcache.revalidate_freq=60

opcache.fast_shutdown=1

opcache.enable_cli=1


Sunday 3 May 2020

Services injection in custom Controller

In custom controller, if it is extending ControllerBase class then no need to inject (with service container) some of the services explained in this post. ControllerBase already having some of the services which we can directly access in the custom controller without injecting it.

Below example will show how to use those services.

Custom Controller Class code

<?php

namespace Drupal\custom_module\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Class ControllerServicesInjectionController.
 *
 * @package Drupal\custom_module\Controller
 */
class ControllerServicesInjectionController extends ControllerBase {

  // Below trait is used to make translatable string in controller class.
  use StringTranslationTrait;

  /**
   * Function having information about services which need not to inject.
   */
  public function useServicesWithoutInjecting() {

// Get current user object without injecting user service.
// Returns the current user.
       $user_object = $this->currentUser();

// Get entityTypeManager service without injecting entityTypeManager service.
// Retrieves the entity type manager.
$entity_type_manager = $this->entityTypeManager();

// Get moduleHandler service without injecting moduleHandler service.
// Returns the module handler.
$module_handler = $this->moduleHandler();

// Get languageManager service without injecting languageManager service.
// Returns the language manager service.
$language_manager = $this->languageManager();

// Get entityFormBuilder service without injecting entityFormBuilder service.
// Retrieves the entity form builder.
$entity_form_builder = $this->entityFormBuilder();

// Get formBuilder service without injecting formBuilder service.
// Returns the form builder service.
$form_builder = $this->formBuilder();

// Get state service without injecting state service.
// Returns the state storage service.
$state_object = $this->state();

// Get keyValue service without injecting keyValue service.
// Returns a key/value storage collection.
$keyvalue_object = $this->keyValue('config_key');

        // Translate string in controller class with StringTranslationTrait.
$this->t('This is translatable string.');

        // To only check the above services output.
        print_r($user_object);
        print_r($entity_type_manager);
        print_r($module_handler);
        print_r($language_manager);
        print_r($entity_form_builder);
        print_r($form_builder);
        print_r($state_object);
        print_r($keyvalue_object);
  }
}

Sunday 26 April 2020

Import Field Configurations Programmatically in Drupal 8

Import Field Configurations Programmatically in Drupal 8


In one of my project, feature module was used to move configurations from one environment to another environment.
With the help of feature module it is easy to move configurations and sync of configurations is easy but when we are moving these configs to production environment we are not having access of it to revert feature changes.

So I decided to write hook_update_n in install file of the module to import configurations of any field. With the help of below code, configurations can be imported to create field.


Create a custom module having below directory structure or place config file in already created custom module in config/install directory and create a modulename.install file

config_import
  config_import.info.yml
  config_import.module
  config_import.install
  config
    install
       // Place all config files here as below
       field.storage.node.field_second_field.yml
       field.field.node.home_stage.field_second_field.yml
       core.entity_form_display.node.home_stage.default.yml
       core.entity_view_display.node.home_stage.default.yml

Code which should available in config_import.install file as below
<?


<?php

/**
 * @file
 * Add search placeholder field.
 */

use Drupal\Core\Config\FileStorage;

/**
 * Add configurations to configs.
 */
function import_config_update_8002() {
  // Get config storage.
  $config_storage = \Drupal::service('config.storage');

  $module = 'import_config';
  // Get config files path.
  $config_path = drupal_get_path('module', 'import_config') . '/config/install';
  // Get file storage for config files.
  $source = new FileStorage($config_path);

  // Create field storage for second field.
  try {
    \Drupal::entityTypeManager()->getStorage('field_storage_config')
      ->create($source->read('field.storage.node.field_second_field'))
      ->save();
  } catch (\Exception $e) {
    Drupal::logger($module)
      ->error('Storage for field field_second_field could not be created. ' . $e->getMessage());
  }

  try {
    // Create field
    \Drupal::entityTypeManager()->getStorage('field_config')
      ->create($source->read('field.field.node.home_stage.field_second_field'))
      ->save();
  } catch (\Exception $e) {
    Drupal::logger($module)
      ->error('Field field_second_field for entity home_stage could not be created. ' . $e->getMessage());
  }

  // Add termscondition field in recipe configurations config pages.
  $config_storage->write('core.entity_form_display.node.home_stage.default', $source->read('core.entity_form_display.node.home_stage.default'));
  $config_storage->write('core.entity_view_display.node.home_stage.default', $source->read('core.entity_view_display.node.home_stage.default'));


}

With above code when you will run drush updb then field "field_second_field" will be created.

For reference :

Saturday 13 April 2019

Alter Path/Route in Drupal 8

In Drupal 7, path can be altered with the help of hook "hook_menu_alter()" but in Drupal 8 hook_menu is no longer exists so to alter the path in Drupal 8, we have to write a route subscriber.

To change the route behaviour, we need to use a service called RouteSubscriber.

  1. We need to tell Drupal that we have this service,
  2. Need to create the right class
Alter the path user/login, user/password handled by user module and remove /search handled by the Search module.
Create a custom module with custom service an RouteSubscriber.

Custom module: route_alter
create a route_alter.services.yml with custom service.

route_alter.info.yml
name: Alter Route
description: 'Alter custom or contributed Route'
type: module
core: 8.x

package: custom

route_alter.services.yml
services:
  route_alter.route_subscriber:
    class: Drupal\route_alter\Routing\AlterCustomRouteSubscriber
    tags:
      - { name: event_subscriber }

To implement RouteSubscriber, create file AlterCustomRouteSubscriber.php in directory src > Routing create class AlterCustomRouteSubscriber within this file.
Copy and paste below code in the above mentioned file.

<?php

/**
 * @file
 * Contains \Drupal\route_alter\Routing\AlterCustomRouteSubscriber.

 */

namespace Drupal\route_alter\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

/**
 * Listens to the dynamic route events.
 */
class AlterCustomRouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  public function alterRoutes(RouteCollection $collection) {
    // Change path '/user/login' to '/login'.
    if ($route = $collection->get('user.login')) {
      $route->setPath('/login');
    }
    // Always deny access to '/user/logout'.
    // Note that the second parameter of setRequirement() is a string.
    if ($route = $collection->get('user.logout')) {
      $route->setRequirement('_access', 'FALSE');
    }
    // Remove the /search route.
    $collection->remove('search.view');
  }
}

Explanation for above code
\Drupal\route_alter\Routing\RouteSubscriber::alterRoutes method is an event subscriber because it extends RouteSubscriberBase. That's why class AlterCustomRouteSubscriber must be registered as an event subscriber service as in file route_alter.services.yml
  • With the help of alterRoutes method, dynamic routes can also added. 
  • If dynamic routes are standalone, use simpler route_callbacks solution and do not implement this class and event subscriber. 
  • If dynamic routes are dependent on other dynamic routes, then need to implement a class extending from RouteSubscriberBase

Thursday 11 April 2019

Function Overriding and Function Overloading in PHP

Function Overriding

What is method overriding in PHP?
Two methods with same name and parameter is called overriding. In Function overriding, both parent class and child class having method with same name and arguments. Purpose of method overriding is to change the behaviour of parent class method.

How to implement overriding in PHP?

Example of overriding in PHP:

<?php
  class parent_class {
    public function text() { //text() is a parent class method
      echo "Hello!! everyone I am parent class text method"."</br>";
    }
    public function test() {
      echo "Hello!! I am second method of parent class"."</br>";
    }
  }

  class child extends parent_class {
    public function text() {  // Text() parent class method which is override by child class
      echo "Hello!! Everyone i am child class";
    }
  }

  $obj = new parent_class();
  echo $obj->text(); // display the parent class method

  $obj= new parent_class();
  echo $obj->test();

  $obj= new child();
  echo $obj->text(); // display the child class method

?>
Output: 

Hello!! Everyone I am parent class text method

Hello!! I am second method of parent class

Hello!! everyone i am child class


As from the above example we can see how text() method of parent class is overridden by child class.

Function Overloading

Function overloading allows to create method with same name but different input parameter.
Method overloading means to assign extra work to same method.

How to implement overloading in PHP?

<?php
  class text {
    public function display($parameter1) {
      echo "Hello world!!";
    }
    public function display($parameter1,$parameter2) {
      echo "Hello India!!”;
    }
  }

  $obj = new text;
  echo $obj->display('Hello'); // It will show fatal error

?>

Output: 

Fatal error: Cannot redeclare text::display() 

Above code will give fatal error. With the above code output, overloading with the same name function cannot be possible. With the help of magic function(__call()) overloading is done. __call() function takes function and arguments.

Example, find the area of certain shapes where radius are given then it should return area of circle if height and width are given then it should give area of rectangle and others.

<?php
  class Shape {
    const Pi = 3.142 ;  // constant value
    
    // __call is magic function which accepts  
    // function name and arguments
    function __call($func_name, $argument) {
      // It will match the function name
      if ($func_name == 'area') {
        switch (count($argument)) {
          // If there is no argument 
  // return 0
  // If there is only one argument 
          // area of circle 
  // If there is two arguments 
          // area of rectangle 
          case 0 : return 0 ;
          case 1 : return self::Pi * $argument[0] ; // 3.14 * 5
          case 2 : return $argument[0] * $argument[1]; // 5 * 10
        }
      }
    }
  }
  $circle = new Shape();
  echo "Area of circle:".$circle->area(5)."</br>"; // display the area of circle
  $rect = new Shape();
  echo "Area of rectangle:".$rect->area(5,10); // display area of rectangle
?>

__call() method automatically called behind the scene.