Skip to main content
Mediacurrent logo
Hero Background Image

Blog Post

Custom Data File Responses in Drupal

by Chris Runo
October 15, 2019

Custom data file exports are a common request from site owners with a large number of intertwined datapoints. For the most part, modules like Views data export can handle this task with ease. However, there are some cases where the data needs to be joined together in a way where creating a view for file exports could prove to be more time consuming than creating a custom route and controller. Saving time on a task like this with a custom implementation could result in that time being spent on more critical components of an application before launch.

Drupal 8 and PHP make it easy to create a custom endpoint for exporting data. In this simplified example, you'll learn how to give a CSV file export of some Article node data combined with user data for all users who are referenced on a given Article.

Start by setting up a new controller. Read through the code comments to follow along:


namespace Drupal\my_module\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\node\Entity\Node;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;

* Class MyCSVReport.
* @package Drupal\my_module\Controller
class MyCSVReport extends ControllerBase implements ContainerInjectionInterface {

  * Drupal\Core\Entity\Query\QueryFactory definition.
  * @var \Drupal\Core\Entity\Query\QueryFactory
 protected $entityQueryFactory;

  * MyCSVReport constructor.
 public function __construct(QueryFactory $entityQueryFactory) {
   // Instantiate dependency injected services.
   // We'll use this to fetch all of our Articles.
   $this->entityQueryFactory = $entityQueryFactory;


  * {@inheritdoc}
 public static function create(ContainerInterface $container) {
   return new static(

  * Export a CSV of data.
 public function build() {
   // Start using PHP's built in file handler functions to create a temporary file.
   $handle = fopen('php://temp', 'w+');

   // Set up the header that will be displayed as the first line of the CSV file.
   // Blank strings are used for multi-cell values where there is a count of
   // the "keys" and a list of the keys with the count of their usage.
   $header = [
     'Article Title',
     'Article Status',
     'Article Author',
     'Referenced Users',
     ' ',
     'Term Reference Field on Users',
     ' ',
   // Add the header as the first line of the CSV.
   fputcsv($handle, $header);
   // Find and load all of the Article nodes we are going to include
   $articles = $this->entityQueryFactory->get('node')
     ->condition('type', article)
   $nodes = $this->entityTypeManager()->getStorage('node')

   // Iterate through the nodes.  We want one row in the CSV per Article.
   foreach ($nodes as $node) {
     // Build the array for putting the row data together.
     $data = $this->buildRow($node);

     // Add the data we exported to the next line of the CSV>
     fputcsv($handle, array_values($data));
   // Reset where we are in the CSV.
   // Retrieve the data from the file handler.
   $csv_data = stream_get_contents($handle);

   // Close the file handler since we don't need it anymore.  We are not storing
   // this file anywhere in the filesystem.

   // This is the "magic" part of the code.  Once the data is built, we can
   // return it as a response.
   $response = new Response();

   // By setting these 2 header options, the browser will see the URL
   // used by this Controller to return a CSV file called "article-report.csv".
   $response->headers->set('Content-Type', 'text/csv');
   $response->headers->set('Content-Disposition', 'attachment; filename="article-report.csv"');

   // This line physically adds the CSV data we created 

   return $response;

  * Fetches data and builds CSV row.
  * @param \Drupal\node\Entity\Node $node
  *   Article node.
  * @return array
  *   Row data.
  * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
  * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 private function buildRow(Node $node) {
   $user_data = $this->getuserData(Node $node);
   $data = [
     'name' => $node->label(),
     'status' => $node->get(status)->value,
     'author' => $node->getOwner()->getDisplayname(),
     'referenced_users_count' => $user_data['total_user_count'],
     'referenced_users' => $user_data['names'],
     'user_term_field_count' => $user_data['term_count'],
     'user_term_field' => $user_data['terms'],

   return $data;

  * Fetches and formats user data for a given Article node.
  * @param \Drupal\node\Entity\Node $node
  *   Article node.
  * @return array
  *   User data.
  * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
  * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 public function getUserData(Node $node) {
  $user_data = [
   'total_user_count' => 0,
   'names' => [],
   'term_count' => 0,
   'terms' => [],

  if (!empty($node->field_referenced_users)) {
   foreach ($node->field_referenced_users>getValue() as $value) {
     $user = $this->entityTypeManager->getStorage('user')
     // Add to the total number of referenced users.
     $user_data['total_user_count'] += 1;

     // Add the user's name as a list item.
     $user_data['names'][] = '- ' . $user->getDisplayname();

     // Now get the term on the user.
     if (!empty($user>field_term_on_user)) {
       $term_field_value = $team_member->field_term_on_user->getValue();
       if (!empty($term_value)) {
         $term = $this->gentityTypeManager->getStorage('taxonomy_term')
         // Check if this term has already been detected on a user on this
         // article.
         if (!array_key_exists($term->label(), $user_data['terms'])) {
           $user_data['term_count'] += 1;
         // Keep track of how many times a term is on a user on the Article.
         $user_data['terms'][$term->label()] += 1;
  // Now iterate through the terms and the term name and count as a single 
  // string.
  foreach ($user_data['terms'] as $term_name => $count) {
    $user_data['names'][] = '- ' . $term_name . ': ' . $count;

    // Now we can remove the array item where we kept track of term counts.
  // Format the terms array into a single string as a list format.
  $user_data['terms'] = implode(PHP_EOL, $user_data['terms']);

  return $user_data;



Once that the controller is set up we can add a route to access the CSV download at:

 path: '/exports/articles'
   _controller: '\Drupal\my_module\Controller\MyCSVReport::build'
   _permission: 'administer users'

Now that both the controller and route are added, users who meet the requirements of the route (in this case have permission to administer users) can access the path /exports/articles and will be presented with a CSV file download containing all of the data we gathered and formatted.

This is what the above code will produce:

code example

Using this example you can further extend the functionality but implementing a form to have certain data be selectable or setup different formats for the data export. On top of that, the data could be exported to any format desirable.  This is not limited to CSV files.  Other popular formats include XLS or ICS files as well.

Chris Runo

Meet team member, Chris Runo

Chris brings four years of Drupal experience to his role as a Senior Drupal Architect&nbsp;at Mediacurrent. Throughout his web development career, Chris has gained a unique perspective on what it takes to deliver a successful project through his experiences in freelancing, agency, and also strictly

Related Insights

  • Chris Runo
  • Chris Runo
  • Chris Runo
  • Chris Runo