This questionnaire is designed to assess knowledge, experience, and expertise in Drupal development. It covers various aspects, including core concepts, module development, theming, site building, and best practices. The responses will help evaluate a candidate's or team's proficiency in working with Drupal projects.
I have extensive experience working with different versions of Drupal, primarily from Drupal 7 to Drupal 10, and I am currently preparing for Drupal 11. Here’s a breakdown of my experience and the key differences I’ve encountered:
Drupal 7
- Hook-based system: Relied heavily on procedural hooks for almost everything.
- Limited Object-Oriented Programming (OOP): Most of the development was procedural PHP.
- No Configuration Management (CMI): Configuration was stored in the database, making deployment difficult.
- Fieldable Entities: Introduced entity and field API but was less flexible than later versions.
- Twig not available: Used PHP template files for theming.
Drupal 8
- OOP and Symfony Integration: Introduced Symfony components, making Drupal more modern and maintainable.
- Configuration Management (CMI): Allowed developers to store configuration in YAML files.
- Twig Template Engine: Replaced PHP templates for more secure and flexible theming.
- Plugin System: Replaced many hooks with plugins (e.g., blocks, field types, etc.).
- REST API Support: Built-in REST services for headless applications.
- Migration System: Introduced a structured migration system for upgrading from Drupal 7.
Drupal 9
- Cleaned up deprecated code: Removed deprecated APIs from Drupal 8.
- Symfony 4.4 / 5.x Upgrade: Ensured better performance and security.
- Backward Compatibility: Made upgrading from Drupal 8 seamless.
- More Composer-Based Workflow: Fully embraced Composer for dependency management.
Drupal 10
- Symfony 6 / PHP 8.1: Improved performance, security, and support for modern PHP.
- Claro Admin Theme: Introduced as the new default admin theme.
- Olivero Frontend Theme: A modern and accessible theme.
- CKEditor 5: Replaced CKEditor 4 for better editing experience.
- jQuery Deprecation: Reduced reliance on jQuery in favor of modern JavaScript.
Drupal 11 (Upcoming)
- Further Symfony and PHP updates: Likely to support Symfony 7 and PHP 8.2+.
- CKEditor 5 Enhancements: More integrations and features.
- Module and Theme Modernization: Expecting continued improvements in APIs and theming.
Key Differences & Challenges
- Migration from Drupal 7: Requires a full content migration due to architectural changes.
- OOP vs. Hooks: Transitioning from procedural hooks in Drupal 7 to an object-oriented approach in Drupal 8+.
- Composer Dependency Management: Essential for Drupal 8+, making module and library management easier but requiring adaptation.
- Theming with Twig: More structured but a shift from direct PHP-based theming in Drupal 7.
- Configuration Synchronization: A game-changer from Drupal 8 onwards, making site deployment much smoother.
I developed a custom module for the Raise Request feature in Drupal 9 for the IIFL Finance project. The module interacts with the getAllProspectApi, applies business logic to validate requests, and ensures that tickets cannot be generated within 30 days from the disbursement date for certain subcategories.
Key Features Implemented
Custom Form for Ticket Creation
- A user-friendly form for submitting requests.
- Prepopulated fields using API responses.
API Integration with getAllProspectApi
- Fetches prospect details dynamically.
- Filters responses based on
Prospect_no
andBranch_Code
.
Business Rule Enforcement
- Implements a condition to prevent ticket generation for specific subcategories within 30 days of the disbursement date.
Validation & Error Handling
- Custom validation for alphanumeric
Prospect_no
andBranch_Code
. - User-friendly error messages.
- Custom validation for alphanumeric
Drupal Messenger for Notifications
- Displays success/error messages to users.
Challenges & Solutions
1. API Response Handling & Performance Issues
- Challenge: The API returned a large dataset, causing performance lags.
- Solution: Implemented batch processing and pagination to optimize API calls and reduce load time.
2. Ensuring Business Rule Enforcement
- Challenge: Validating 30-day restrictions dynamically based on API data.
- Solution: Converted the
disbursement_date
from API response to a timestamp and checked it against the current date before allowing submission.
3. Drupal Form Validation Issues
- Challenge: The validation for
Prospect_no
andBranch_Code
needed to be robust against user input errors. - Solution: Used #element_validate and regex patterns to ensure only alphanumeric values were accepted.
4. Managing API Failures & Error Handling
- Challenge: If the API was down, the form had to handle failures gracefully.
- Solution: Implemented try-catch blocks, displayed fallback messages, and logged errors for debugging.
I have worked extensively on Drupal upgrades, including moving sites from Drupal 8 to 9 and Drupal 9 to 10. Most recently, I have been preparing for the Drupal 10 upgrade for the IIFL Finance project.
Key Steps in the Upgrade Process
Assessing Compatibility with
upgrade_status
- Used the Upgrade Status module to scan for deprecated code and incompatible modules.
- Identified contrib and custom modules that needed updates.
Updating Contributed Modules & Themes
- Checked module compatibility via Drupal.org.
- Updated dependencies using Composer (
composer update
). - Replaced deprecated modules like CKEditor 4 with CKEditor 5.
Custom Code Refactoring
- Removed deprecated functions (
drupal_set_message()
,drupal_render()
, etc.). - Updated service definitions to align with Symfony 6.
- Replaced deprecated jQuery functions with modern JavaScript.
- Removed deprecated functions (
Database Schema & Configuration Updates
- Ran
drush updb
for database updates. - Exported and imported configurations (
drush cex && drush cim
).
- Ran
Testing & Debugging
- Conducted functional testing to verify form submissions, API calls, and UI interactions.
- Used PHPStan and Drupal Rector to scan for deprecated code.
- Debugged issues using devel module and Drupal logs.
Challenges & Solutions
1. Deprecated Code in Custom Modules
- Challenge: Some custom modules had deprecated functions that caused errors.
- Solution: Used Drupal Rector to automate code refactoring and manually replaced unsupported functions.
2. CKEditor 5 Migration Issues
- Challenge: CKEditor 5 required new configurations and plugins.
- Solution: Reconfigured text formats and enabled necessary plugins (
Link
,Image
,Table
, etc.).
3. Contrib Module Incompatibility
- Challenge: Some contrib modules were not yet ready for Drupal 10.
- Solution:
- Checked Drupal.org for patches.
- Used
composer prohibit
to block incompatible modules. - Replaced unsupported modules with alternatives.
4. Symfony 6 and PHP 8.1 Compatibility
- Challenge: Some functions and services had breaking changes due to Symfony updates.
- Solution:
- Updated custom service definitions to use dependency injection properly.
- Used
composer why-not drupal/core
to check compatibility issues.
5. Twig and Theming Issues
- Challenge: Some template files (
.twig
) used deprecated syntax. - Solution: Updated templates based on Drupal 10 theming guidelines.
n Drupal, both hooks and event subscribers allow developers to modify and extend functionality, but they differ in approach and use cases. Here’s when you would choose one over the other:
When to Use Hooks
- Modify core/module behavior quickly: Hooks are great for overriding or altering existing functionality (e.g.,
hook_form_alter()
to modify a form). - Procedural simplicity: If you need a simple modification without requiring additional service definitions, a hook is more straightforward.
- Execution order is not critical: Hooks execute in a predefined sequence but may not allow precise control over execution order.
When to Use Event Subscribers
- More structured and reusable code: Since event subscribers use object-oriented programming, they are better for maintainability.
- Need for execution control: Event subscribers allow priority levels to be set, letting you control when they execute relative to other listeners.
- Reacting to Symfony events: If you need to work with Drupal’s event-driven architecture (like kernel requests, responses, or entity CRUD operations), event subscribers are required.
- When multiple modules listen to the same event: Event subscribers let multiple modules react to the same event in a controlled way.
Examples Where Event Subscribers Are Better Than Hooks
1. Modifying Responses for Specific Routes (Kernel Events)
If you need to modify responses before they are sent to the user, using an event subscriber is ideal because it listens to KernelEvents::RESPONSE
.
Example: Altering JSON Responses
- Why not use a hook? There’s no hook equivalent for modifying responses at the Symfony level.
2. Handling Entity CRUD Events (e.g., User Login, Node Creation)
Event subscribers are useful for executing custom logic when entities are created, updated, or deleted.
Example: Logging Every New Node Creation
- Why not use
hook_ENTITY_TYPE_insert()
?- Hooks only apply per entity type (
hook_node_insert()
,hook_user_insert()
), whereas event subscribers can handle multiple entity types dynamically.
- Hooks only apply per entity type (
3. Running Custom Logic Before Sending an Email (Mail System)
If you need to intercept and modify emails before they are sent, event subscribers provide a structured approach.
Example: Adding a Custom Footer to Emails
- Why not use
hook_mail_alter()
?hook_mail_alter()
runs before the mail is sent but doesn’t provide as much flexibility in execution control.
Conclusion: When to Choose What?
Scenario | Use a Hook | Use an Event Subscriber |
---|---|---|
Modifying forms (e.g., adding fields, validation) | ✅ hook_form_alter() | 🚫 |
Altering entities when they are created, updated, or deleted | ✅ hook_ENTITY_TYPE_insert() | ✅ More flexible for multiple entities |
Modifying HTTP responses | 🚫 | ✅ KernelEvents::RESPONSE |
Logging user logins or node creation | ✅ hook_user_login() | ✅ More structured, OOP approach |
Sending or modifying emails | ✅ hook_mail_alter() | ✅ More control with MailSendEvent |
In short: Use hooks for simple modifications where procedural logic is sufficient. Use event subscribers for structured, reusable, and event-driven logic.
Drupal’s plugin system is a flexible way to extend functionality without modifying core or contrib modules. It provides a structured, reusable approach to implementing features.
Examples of Custom Plugins I’ve Developed
1. Custom Field Formatter Plugin
Use case: Formatting a field differently based on business logic.
💡 Example: In an Auction system, we needed to display the bid_amount
field differently for active and expired auctions.
Implementation:
- Define the Plugin (
src/Plugin/Field/FieldFormatter/BidAmountFormatter.php
)
namespace Drupal\custom_module\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* @FieldFormatter(
* id = "bid_amount_formatter",
* label = @Translation("Bid Amount Formatter"),
* field_types = {
* "decimal"
* }
* )
*/
class BidAmountFormatter extends FormatterBase {
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
$bid_amount = $item->value;
$status = $items->getEntity()->get('status')->value;
$formatted_value = $status === 'active'
? "<strong>$bid_amount</strong> (Live Auction)"
: "<span style='color:gray;'>$bid_amount</span> (Expired)";
$elements[$delta] = ['#markup' => $formatted_value];
}
return $elements;
}
}
Why a plugin?
- Using a Field Formatter Plugin allows us to format the field dynamically instead of altering the template.
2. Custom Block Plugin
Use case: Displaying dynamic loan eligibility calculations in a Banking/Finance website.
💡 Example: A Loan Calculator Block that users can place anywhere, fetching real-time interest rates.
Implementation:
- Define the Plugin (
src/Plugin/Block/LoanCalculatorBlock.php
)
namespace Drupal\custom_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* @Block(
* id = "loan_calculator_block",
* admin_label = @Translation("Loan Calculator Block")
* )
*/
class LoanCalculatorBlock extends BlockBase {
public function build() {
return [
'#markup' => $this->generateCalculatorForm(),
'#cache' => ['max-age' => 0], // Disable cache for dynamic updates.
];
}
private function generateCalculatorForm() {
return '<form>
<label>Loan Amount:</label>
<input type="text" name="loan_amount">
<button type="submit">Calculate</button>
</form>';
}
}
Why a plugin?
- Reusable: The block can be placed on any page.
- Dynamic logic: Can be extended to use AJAX for real-time calculations.
3. Custom Queue Worker Plugin
Use case: Handling large-scale CSV imports asynchronously in the IIFL Finance project.
💡 Example: Processing auction properties CSV using a queue system.
Implementation:
- Define the Plugin (
src/Plugin/QueueWorker/AuctionPropertyProcessor.php
)
namespace Drupal\custom_module\Plugin\QueueWorker;
use Drupal\Core\Queue\QueueWorkerBase;
/**
* @QueueWorker(
* id = "auction_property_processor",
* title = @Translation("Auction Property Processor"),
* cron = {"time" = 60}
* )
*/
class AuctionPropertyProcessor extends QueueWorkerBase {
public function processItem($data) {
\Drupal::logger('custom_module')->notice('Processing auction property: @property', [
'@property' => $data['property_name']
]);
// Process auction property data
// Example: Save it as a node or update existing data.
}
}
Why a plugin?
- Asynchronous processing: Large CSVs don’t block user actions.
- Performance: Helps optimize performance by breaking the processing into batches.
4. Custom Migrate Source Plugin
Use case: Custom data migration from a third-party API to Drupal.
💡 Example: Migrating loan application data from an external API into Drupal nodes.
Implementation:
- Define the Plugin (
src/Plugin/migrate/source/LoanApplication.php
)
namespace Drupal\custom_module\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use GuzzleHttp\Client;
/**
* @MigrateSource(
* id = "loan_application"
* )
*/
class LoanApplication extends SourcePluginBase {
public function initializeIterator() {
$client = new Client();
$response = $client->get('https://api.example.com/loan-applications');
$data = json_decode($response->getBody(), TRUE);
return new \ArrayIterator($data);
}
}
Why a plugin?
- Custom API integration: Fetches external data instead of relying on a static CSV.
- Extensible: Can be modified for different data structures.
Conclusion: When to Use a Plugin in Drupal?
Use Case | Type of Plugin | Benefit |
---|---|---|
Customizing how a field is displayed | Field Formatter | Clean, reusable, and configurable UI formatting |
Creating a reusable site block | Block Plugin | Easily configurable and placed anywhere |
Processing background tasks (e.g., CSV imports) | Queue Worker Plugin | Asynchronous execution for better performance |
Migrating external data (e.g., API or database) | Migrate Source Plugin | Fetches and processes data dynamically |
Understanding Dependency Injection in Drupal 8 & 9
Dependency Injection (DI) is a design pattern where objects receive their dependencies (services) instead of creating them manually. In Drupal 8 and 9, DI is widely used to promote better code reusability, testability, and maintainability by injecting services rather than using global calls like \Drupal::service()
.
Why Use Dependency Injection?
Feature | Global Calls (\Drupal::service()) | Dependency Injection |
---|---|---|
Testability | Hard to mock services for unit tests | Easily mock services |
Performance | Multiple calls can create redundant instances | Injected once, improving efficiency |
Maintainability | Tight coupling with global state | Loose coupling, making code reusable |
Code Clarity | Hard to trace dependencies | Clearly defined dependencies in constructors |
How to Use Dependency Injection in Custom Modules?
1. Using DI in a Custom Service
💡 Example Use Case: Logging auction property updates.
Step 1: Define the Service (custom_module.services.yml
)
services:
custom_module.logger_service:
class: Drupal\custom_module\Service\LoggerService
arguments: ['@logger.factory']
Step 2: Create the Service Class (src/Service/LoggerService.php
)
namespace Drupal\custom_module\Service;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
class LoggerService {
protected $logger;
public function __construct(LoggerChannelFactoryInterface $loggerFactory) {
$this->logger = $loggerFactory->get('custom_module');
}
public function logMessage($message) {
$this->logger->info($message);
}
}
Step 3: Inject the Service into a Controller (src/Controller/CustomController.php
)
namespace Drupal\custom_module\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\custom_module\Service\LoggerService;
class CustomController extends ControllerBase {
protected $loggerService;
public function __construct(LoggerService $loggerService) {
$this->loggerService = $loggerService;
}
public static function create(ContainerInterface $container) {
return new static($container->get('custom_module.logger_service'));
}
public function logAuctionUpdate() {
$this->loggerService->logMessage('Auction property updated.');
return ['#markup' => 'Log entry added.'];
}
}
Advantages of DI here:
- The
LoggerService
is injected instead of using\Drupal::logger()
. - This makes it mockable for testing and loosely coupled.
2. Injecting Services in Plugins (Example: Block Plugin)
💡 Example Use Case: Fetching dynamic interest rates from a configuration service.
Step 1: Inject the Config Factory in a Block Plugin
namespace Drupal\custom_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @Block(
* id = "loan_interest_rate_block",
* admin_label = @Translation("Loan Interest Rate Block")
* )
*/
class LoanInterestRateBlock extends BlockBase {
protected $configFactory;
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $configFactory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $configFactory;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('config.factory'));
}
public function build() {
$interest_rate = $this->configFactory->get('custom_module.settings')->get('interest_rate');
return ['#markup' => "Current Loan Interest Rate: {$interest_rate}%"];
}
}
Advantages of DI here:
- No
\Drupal::config()
global call, making the block independent of the global state. - Easier unit testing by mocking the
config.factory
service.
3. Injecting Services in an Event Subscriber
💡 Example Use Case: Logging a message when a user logs in.
Step 1: Define the Event Subscriber (src/EventSubscriber/UserLoginSubscriber.php
)
namespace Drupal\custom_module\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\RequestEvent;
class UserLoginSubscriber implements EventSubscriberInterface {
protected $logger;
public function __construct(LoggerChannelFactoryInterface $loggerFactory) {
$this->logger = $loggerFactory->get('user_activity');
}
public static function getSubscribedEvents() {
return [KernelEvents::REQUEST => ['logUserLogin', 0]];
}
public function logUserLogin(RequestEvent $event) {
$this->logger->info('User has logged in.');
}
}
Advantages of DI here:
- The logger is injected, making it easy to test.
- No global calls, keeping the subscriber clean and reusable.
Key Takeaways
🔹 Dependency Injection helps in:
- Testability: You can easily mock services in unit tests.
- Performance: Services are loaded only when needed, avoiding redundant calls.
- Maintainability: Code is more structured and reusable.
- Encapsulation: Dependencies are clearly defined in constructors.
🔹 Use Dependency Injection when:
✅ You need a reusable service that can be injected anywhere.
✅ You're working with plugins, blocks, controllers, or event subscribers.
✅ You want to avoid global function calls (\Drupal::service()
), making your code cleaner.