Fantasy Baseball Statistics

Introduction

I’ve been playing fantasy baseball for years and I like to keep track of my statistics from year to year. It helps me set goals for stat totals and I also enjoy comparing how I’m doing from year to year. During the season, I write down my daily statistics in a spreadsheet. At the end of the year, I record all of the different players who earned statistics for my team and how much they earned. I play the rotisserie style in yahoo sports which ranks yearly totals of batting average, runs, rbis, stolen bases, home runs, wins, saves, strike outs, era, and whip. I also like to score each individual stat line in order to compare my results. This way I can be a better judge of who is having a good fantasy season and who is not. Otherwise I sometimes overvalue statistics like batting average or era.

I found the spreadsheets to be limiting for some of the stats I wanted to compare, so I ended up putting the data into a database (Mysql). This allowed me to run queries and get answers quite easily. However, I also wanted a simple interface for viewing the results. I decided to add a coding framework as a front and back end for visualize the data and running queries as needed. I decided to use Drupal as a this framework simply because I am intimately familiar with how to code for Drupal and I was interested in building this quickly.

Database Tables

I broke up the data into 4 different tables:

  • Hitting Daily (Records the daily hitting statistics for the entire team. There is a new line for each day)
  • Hitting Players (Records the season total for each individual player. Only statistics received while in the lineup count)
  • Pitching Daily (Same as hitting daily but for pitchers)
  • Pitching Players (Same as hitting players but for pitchers)

I could have reduced this to 2 tables by recording each individual’s statistics on a daily basis but I didn’t have all of that information when I started this and it was too tedious anyway.

Also, these tables are only recording raw data. Any computed data, such as batting average, or the stat line score will all be live computed.

Here’s an example of the ‘Hitting Players’ table:

id name teams positions rank position_rank year games hits at_bats runs home_runs rbis stolen_bases
325 Jorge Soler KC/ATL OF 309 65 2021 29 27 116 17 6 12 0
311 Aaron Judge NYY OF 33 9 2021 145 157 547 89 39 97 6
181 Mike Napoli CLE 1B,OF 86 15,19 2016 81 69 298 46 19 48 1
36 Aaron Hill TOR 2B NULL NULL 2011 15 10 58 9 1 6 3

Class Construction

So, looking at the given data, I decided to build 2 different types of PHP classes (which would became custom Drupal plugins):

  • Stat: Provides methods for handling operations involving a single statistic. For example, ‘Runs’ would be a type of Stat class. This class would know where the stat is stored and can provide the values when requested. Some of these will be computed stats and not stored but have to rely on raw stats. I’ll get more into this later.
  • Statline: Provides methods for handling operations on a single line of statistics. For example, in the table above, each individual row would be a statline. It’s made up of a bunch of Stats. This class would compile all of the stats so it can run queries and also compute a stat line score in order to compare with others. The details of scoring a stat or statline or left out of this post.

When I looked at the individual stats, I realized that I’d have to add a class for each column in the database, and I’d also have a number of computed stats like batting average, era, and whip, and also each stat’s score which I use to compare their value. The computed stats were going to rely on the raw data stats, so when loaded up by a parent statline, they would have to be loaded in proper order. It can’t compute batting average without hits and at-bats.

As for statlines, I was going to need to make 4 of these types of classes with each one corresponding to one of the tables I noted above. They were going to all be similar and each one would define which stats to be associated.

My ultimate goal with these classes was to be able to run queries from the statline that could get me totals, averages, scores, and to group results for display. I wanted to see things like a list of my monthly stats or yearly stats in a table and to be able to sort by the rows. I also wanted to be able to filter players by position.

I later add code to be able to enter a statline into a form and have totals computed so I could score a statline not currently in the system. I’m not going to address that in this post, but the code for it is in here.

Drupal Custom Plugin

Everything done here is performed in a custom module. For this post, I titles it my_module and I’m leaving out details of setting up a custom module. You can determine where to put the files based on their namespaces. To make custom Drupal plugins, I first had to define the manager classes in my module services file (my_module.services.yml]):

services:
  plugin.manager.my_module.stat_line_manager:
    class: Drupal\my_module\Plugin\StatLine\StatLineManager
    parent: default_plugin_manager

  plugin.manager.my_module.stat_manager:
    class: Drupal\my_module\Plugin\Stat\StatManager
    parent: default_plugin_manager

Then, I had to define the manager classes. It was boilerplate code so I’m only posting StatManager.php below:

<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;

/**
 * Manage Stats.
 */
class StatManager extends DefaultPluginManager {

  /**
   * Constructs an StatManager object.
   *
   * @param \Traversable $namespaces
   *   An object that implements \Traversable which contains the root paths
   *   keyed by the corresponding namespace to look for plugin implementations.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   Cache backend instance to use.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler to invoke the alter hook with.
   */
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct(
      'Plugin/Stat',
      $namespaces,
      $module_handler,
      'Drupal\my_module\Plugin\Stat\StatPluginInterface',
      'Drupal\my_module\Annotation\Stat'
    );

    $this->alterInfo('my_module_stat_info');
    $this->setCacheBackend($cache_backend, 'my_module.stat');
  }

}

The above code references a custom plugin interface and an annotation plugin. The annotation plugins here are just boiler plate code so I’m not posting. The plugin interfaces, though are where we define the methods that the classes will need to run, but not the specific code:

<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\Component\Plugin\PluginInspectionInterface;

/**
 * Defines an interface for a stat.
 */
interface StatPluginInterface extends PluginInspectionInterface {

  /**
   * Return the name of the name of the service.
   *
   * @return string
   */
  public function getName();

  /**
   * Get the related database column for this stat.
   *
   * @param string|NULL $group_type
   *  (optional) The grouping being done on the related query.
   *
   * @return string|NULL
   */
  public function getColumn($group_type = NULL);

  /**
   * Get the number of decimals to display on this stat.
   *
   * @return int|NULL
   */
  public function getDecimals();

  /**
   * Whether a smaller number is best or larger (min or max).
   *
   * @return string
   */
  public function getBestOrder();

  /**
   * Whether this stat should be included when totaling the stats.
   *
   * @return bool
   */
  public function includeInTotal();

  /**
   * Whether this stat is computed from other data or not.
   *
   * @return bool
   */
  public function computed();

  /**
   * Whether to display this stat or not.
   *
   * @return bool
   */
  public function display();

  /**
   * The sql alias for this expression.
   *
   * @return string
   */
  public function getExpressionAlias();

  /**
   * Returns the value stored on this stat if it's a single piece of data.
   *
   * @return bool
   */
  public function getValue();

  /**
   * Set the value on a single stat.
   *
   * @param $value
   */
  public function setValue($value);

  /**
   * Get the query expression for this stat.
   *
   * @param array $groups
   *  (optional) A list of groupings being used for the overall query.
   * @param string $group_type
   *  (optional) The type of grouping being run (SUM, AVERAGE).
   *
   * @return string
   */
  public function getExpression(array $groups = [], $group_type = 'SUM');

  /**
   * Compute the value on this single stat.
   *
   * @return mixed
   */
  public function compute();

  /**
   * Get a required stat on this stat.
   *
   * @return Drupal\my_module\Plugin\Stat\StatPluginInterface
   */
  public function getStat(string $id);

}
<?php

namespace Drupal\my_module\Plugin\StatLine;

use Drupal\Component\Plugin\PluginInspectionInterface;

/**
 * Defines an interface for a stat line.
 */
interface StatLinePluginInterface extends PluginInspectionInterface {

  /**
   * Return the name of the name of the service.
   *
   * @return string
   */
  public function getName();

  /**
   *  Get the database connection.
   *
   * @return \Drupal\Core\Database\Connection
   */
  public function getDatabase();

  /**
   *  Get the stat manager service.
   *
   * @return \Drupal\my_module\Plugin\Stat\StatManager
   */
  public function getStatManager();

  /**
   * Get the related stats.
   *
   * @return array \Drupal\my_module\Plugin\Stat\StatPluginInterface
   */
  public function getStats();

  /**
   * Get the table header data.
   *
   * @return array
   */
  public function getHeader();

  /**
   * Get the results from the query execution.
   *
   * @return
   *   An array of results.
   */
  public function results();

  /**
   * Whether the data is stored in a db table or must be computed.
   *
   * @return bool
   */
  public function statsInDb();

  /**
   * Get the titles of all related stats.
   *
   * @return array
   */
  public function getStatTitles();

  /**
   * Get the sql expressions from the related stats.
   *
   * @return array
   */
  public function getStatMetaData();

  /**
   * Execute the query to get all stat data.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function executeQuery();

  /**
   * Add a modifier to filter/sort/group the query results.
   *
   * @param string $type
   *  The type of modifier (filter, sort, group, group_type)
   * @param mixed $modifier
   *  The value of the modifier.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function addModifier(string $type, $modifier);

  /**
   * Set the modifier a value.
   *
   * @param string $type
   *  The type of modifier (filter, sort, group, group_type)
   * @param array $modifier
   *  The value of the modifier.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function setModifier(string $type, array $modifiers);

  /**
   * Set the value on all of the modifiers.
   *
   * @param array $modifiers
   *  An array containing all of the modifiers.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function setModifiers(array $modifiers);

  /**
   * Set the value on related stats.
   *
   * @param array $stat_values
   *  An array of values keyed by the stat id.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function setStatValues(array $stat_values);

  /**
   * Calculate the computed values on the stats.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function compute();

}

Then, I defined the base functioning class for Stats. This would be the base class for any raw data value statistic but wouldn’t be used directly:

<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\my_module\Plugin\Stat\StatPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Defines a base plugin for a Stat.
 */
abstract class StatPluginBase extends PluginBase implements StatPluginInterface, ContainerFactoryPluginInterface {

  use StringTranslationTrait;

  /**
   * Whether a higher or lower number is better (min or max).
   *
   * @var string
   */
  protected $best = 'max';

  /**
   * The name of the related sql table column.
   *
   * @var string
   */
  protected $column;

  /**
   * Whether or not this is a computed value.
   *
   * @var boolean
   */
  protected $computed = FALSE;

  /**
   * The number of decimals to display on this number.
   *
   * @var int
   */
  protected $decimals = 0;

  /**
   * Whether or not to display the stat.
   *
   * @var boolean
   */
  protected $display = TRUE;

  /**
   * The sql alias to use.
   *
   * @var string
   */
  protected $expressionAlias;

  /**
   * The required stat objects.
   *
   * @var array
   */
  protected $requiredStats = [];

  /**
   * The plugin ids of the required stats.
   *
   * @var array
   */
  protected $requiredStatIds = [];

  /**
   * Whether or not this is used to compute the total score.
   *
   * @var boolean
   */
  protected $total = FALSE;

  /**
   * The value when used as a single stat.
   *
   * @var mixed
   */
  protected $value;

  /**
   * Constructs a Drupal\my_module\Plugin\Stat\StatPluginBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    if (!empty($this->requiredStatIds)) {
      foreach ($this->requiredStatIds as $id) {
        if (empty($configuration['stats'][$id])) {
          throw PluginException(sprintf('Missing required stat %s', $id));
        }
        $this->requiredStats[$id] = $configuration['stats'][$id];
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getName() {
    return $this->pluginDefinition['name'];
  }

  /**
   * {@inheritdoc}
   */
  public function getColumn($group_type = NULL) {
    return $group_type ? $group_type . '(' . $this->column . ')' : $this->column;
  }

  /**
   * {@inheritdoc}
   */
  public function getDecimals() {
    return $this->decimals;
  }

  /**
   * {@inheritdoc}
   */
  public function getBestOrder() {
    return $this->best;
  }

  public function includeInTotal() {
    return $this->total;
  }

  /**
   * {@inheritdoc}
   */
  public function computed() {
    return $this->computed;
  }

  /**
   * {@inheritdoc}
   */
  public function display() {
    return $this->display;
  }

  /**
   * {@inheritdoc}
   */
  public function getExpressionAlias() {
    return $this->expressionAlias;
  }

  /**
   * {@inheritdoc}
   */
  public function getValue() {
    return $this->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setValue($value) {
    $this->value = $value;
  }

  /**
   * {@inheritdoc}
   */
  public function getExpression(array $groups = [], $group_type = 'SUM') {
    return $this->getColumn(!empty($groups) ? $group_type : NULL);
  }

  /**
   * {@inheritdoc}
   */
  public function compute() {
    return $this->getValue();
  }

  /**
   * {@inheritdoc}
   */
  public function getStat(string $id) {
    return $this->requiredStats[$id];
  }
}

Here are 3 examples of stats using the above bae plugin which demonstrates the different ways it was used:

<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\my_module\Plugin\Stat\StatPluginBase;

/**
 * Provides runs stat.
 *
 * @Stat(
 *   id = "runs",
 *   name = @Translation("Runs"),
 * )
 */
class Runs extends StatPluginBase {
  protected $column = 'runs';
}

<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\my_module\Plugin\Stat\StatPluginBase;

/**
 * Provides player name stat.
 *
 * @Stat(
 *   id = "name",
 *   name = @Translation("Name"),
 * )
 */
class Name extends StatPluginBase {
  protected $best = NULL;
  protected $column = 'name';
  protected $decimals = NULL;
}
<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\my_module\Plugin\Stat\StatPluginBase;
use Drupal\Component\Plugin\Exception\PluginException;

/**
 * Provides batting average stat.
 *
 * @Stat(
 *   id = "batting_average",
 *   name = @Translation("BA"),
 * )
 */
class BattingAverage extends StatPluginBase {
  protected $computed = TRUE;
  protected $decimals = 3;
  protected $requiredStatIds = [
    'at_bats',
    'hits'
  ];

  /**
   * {@inheritdoc}
   */
  public function compute() {
    $value = $this->getStat('hits')->getValue() / $this->getStat('at_bats')->getValue();
    $this->setValue($value);
    return $value;
  }

  /**
   * {@inheritdoc}
   */
  public function getExpression(array $groups = [], $group_type = 'SUM') {
    if (empty($groups)) {
      $group_type = NULL;
    }
    return $this->getStat('hits')->getColumn($group_type) . ' / ' . $this->getStat('at_bats')->getColumn($group_type);
  }
}

You can see the the batting average stat requires to be provided with a hit and at-bats stat and also defines how to compute itself both in a query and also in php.

Next, I defined the base stat class for statistics totals. These are the stats that take a statistic and assign a score in order to compare the statistic against other statistics:

<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\my_module\Plugin\Stat\StatPluginBase;

/**
 * Definition of a basic total stat for scoring a stat.
 */
abstract class StatTotal extends StatPluginBase {
  /**
   * The value used to compute the total value.
   *
   * @var float
   */
  protected $constant;

  protected $computed = TRUE;
  protected $total = TRUE;

  /**
   * {@inheritdoc}
   */
  public function getExpression(array $groups = [], $group_type = 'SUM') {
    $group_type = !empty($groups) ? $group_type : NULL;
    return $this->getBaseStat()->getColumn($group_type) . ' * ' . $this->getConstant();
  }

  /**
   * {@inheritdoc}
   */
  public function compute() {
    $value = $this->getBaseStat()->getValue() * $this->getConstant();
    $this->setValue($value);
    return $value;
  }

  /**
   * Get the plugin id of the base stat
   *  A basic total is based on one required stat.
   *
   * @return string
   */
  public function getBaseStatId() {
    return reset($this->requiredStatIds);
  }

  /**
   * Get the constant for computing the value.
   *
   * @return float
   */
  public function getConstant() {
    return $this->constant;
  }

  /**
   * Get the related base stat instance.
   *
   * @return Drupal\my_module\Plugin\Stat\StatPluginInterface
   */
  public function getBaseStat() {
    return $this->getStat($this->getBaseStatId());
  }
}

Here’s an example of a stat that uses this as a base:

<?php

namespace Drupal\my_module\Plugin\Stat;

use Drupal\my_module\Plugin\Stat\StatTotal;

/**
 * Provides runs total stat.
 *
 * @Stat(
 *   id = "runs_total",
 *   name = @Translation("Runs-Tot"),
 * )
 */
class RunsTotal extends StatTotal {
  protected $constant = 2.25;
  protected $requiredStatIds = [
    'runs'
  ];
}

Next comes the StateLine classes. The manager class is pretty much the same as the Stat manager class but uses the names StatLine instead of Stat. Here’s the plugin interface:

<?php

namespace Drupal\my_module\Plugin\StatLine;

use Drupal\Component\Plugin\PluginInspectionInterface;

/**
 * Defines an interface for a stat line.
 */
interface StatLinePluginInterface extends PluginInspectionInterface {

  /**
   * Return the name of the name of the service.
   *
   * @return string
   */
  public function getName();

  /**
   *  Get the database connection.
   *
   * @return \Drupal\Core\Database\Connection
   */
  public function getDatabase();

  /**
   *  Get the stat manager service.
   *
   * @return \Drupal\my_module\Plugin\Stat\StatManager
   */
  public function getStatManager();

  /**
   * Get the related stats.
   *
   * @return array \Drupal\my_module\Plugin\Stat\StatPluginInterface
   */
  public function getStats();

  /**
   * Get the table header data.
   *
   * @return array
   */
  public function getHeader();

  /**
   * Get the results from the query execution.
   *
   * @return
   *   An array of results.
   */
  public function results();

  /**
   * Whether the data is stored in a db table or must be computed.
   *
   * @return bool
   */
  public function statsInDb();

  /**
   * Get the titles of all related stats.
   *
   * @return array
   */
  public function getStatTitles();

  /**
   * Get the sql expressions from the related stats.
   *
   * @return array
   */
  public function getStatMetaData();

  /**
   * Execute the query to get all stat data.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function executeQuery();

  /**
   * Add a modifier to filter/sort/group the query results.
   *
   * @param string $type
   *  The type of modifier (filter, sort, group, group_type)
   * @param mixed $modifier
   *  The value of the modifier.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function addModifier(string $type, $modifier);

  /**
   * Set the modifier a value.
   *
   * @param string $type
   *  The type of modifier (filter, sort, group, group_type)
   * @param array $modifier
   *  The value of the modifier.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function setModifier(string $type, array $modifiers);

  /**
   * Set the value on all of the modifiers.
   *
   * @param array $modifiers
   *  An array containing all of the modifiers.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function setModifiers(array $modifiers);

  /**
   * Set the value on related stats.
   *
   * @param array $stat_values
   *  An array of values keyed by the stat id.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function setStatValues(array $stat_values);

  /**
   * Calculate the computed values on the stats.
   *
   * @return \Drupal\my_module\Plugin\StatLine\StatLinePluginInterface
   *  The current object.
   */
  public function compute();

}

This gives us the ability to grab all of the raw statistics from the database, compute the computed statistics, add filters and modifiers to any query, and get metadata info about the stats as well. Here’s the code from the StatLine base plugin:

<?php

namespace Drupal\my_module\Plugin\StatLine;

use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\my_module\Plugin\Stat\StatManager;
use Drupal\my_module\Plugin\StatLine\StatLinePluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Defines an interface for a stat line.
 */
abstract class StatLinePluginBase extends PluginBase implements StatLinePluginInterface, ContainerFactoryPluginInterface {

  use StringTranslationTrait;

  /**
   * Active database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * Stat Manager service.
   *
   * @var \Drupal\my_module\Plugin\Stat\StatManager
   */
  protected $statManager;

  /**
   * The sql database name related to this stat line.
   *
   * @var string
   */
  protected $table;

  /**
   * List of related stat ids in display order.
   *
   * @var array
   */
  protected $statOrder;

  /**
   * List of stat instances related to this stat line.
   *
   * @var array \Drupal\my_module\Plugin\Stat\StatPluginInterface
   */
  protected $stats = [];

  /**
   * Metadata about each stat.
   *
   * @var array
   */
  protected $statMetaData = [];

  /**
   * The name of the stat to sort by default.
   *
   * @var string
   */
  protected $defaultSort;

  /**
   * Table header data for displaying stats.
   *
   * @var array
   */
  protected $header = [];

  /**
   * The query results are stored here.
   *
   * @var array
   */
  protected $results;

  /**
   * Allowed modifiers for the sql expression.
   *
   * @var array
   */
  protected $modifiers = [
    'sorts' => [],
    'filters' => [],
    'groups' => [],
    'group_type' => 'SUM',
  ];

  /**
   * Constructs a Drupal\my_module\Plugin\StatLine\StatLinePluginBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Database\Connection $database
   *   The current database connection.
   * @param \Drupal\my_module\Plugin\Stat\StatManager
   *   The stat manager service.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, StatManager $statManager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->database = $database;
    $this->statManager = $statManager;

    foreach ($this->statOrder as $id) {
      // \Drupal\Component\Plugin\Exception\PluginException thrown
      // if id does not exist or if required stats not there.
      $this->stats[$id] = $this->getStatManager()->createInstance($id, ['stats' => $this->stats]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('database'),
      $container->get('plugin.manager.my_module.stat_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getName() {
    return $this->pluginDefinition['name'];
  }

  /**
   * {@inheritdoc}
   */
  public function getDatabase() {
    return $this->database;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatManager() {
    return $this->statManager;
  }

  /**
   * {@inheritdoc}
   */
  public function getStats() {
    return $this->stats;
  }

  /**
   * {@inheritdoc}
   */
  public function getHeader() {
    if (!empty($this->header)) {
      return $this->header;
    }

    foreach ($this->getStatMetaData() as $field => $data) {
      $alias = !empty($data['alias']) ? $data['alias'] : $field;
      if (!empty($data['display'])) {
        $this->header[] = ['data' => $data['title'], 'field' => $alias, 'sort' => 'desc', 'sort' => $field === $this->defaultSort ? 'desc' : NULL];
      }
    }
    return $this->header;
  }

  /**
   * {@inheritdoc}
   */
  public function results() {
    return $this->results;
  }

  /**
   * {@inheritdoc}
   */
  public function statsInDb() {
    return isset($this->table);
  }

  /**
   * {@inheritdoc}
   */
  public function getStatTitles() {
    $statMetaData = $this->getStatMetaData();
    return array_column($statMetaData, 'title');
  }

  /**
   * {@inheritdoc}
   */
  public function getStatMetaData() {
    if (!empty($this->statMetaData)) {
      return $this->statMetaData;
    }

    $statMetaData = [];
    foreach ($this->getStats() as $id => $stat) {
      if ($expression = $stat->getExpression($this->modifiers['groups'], $this->modifiers['group_type'])) {
        $this->statMetaData[$id] = [
          'expression' => $expression,
          'display' => $stat->display(),
          'title' => $stat->getName(),
          'best' => $stat->getBestOrder(),
          'decimals' => $stat->getDecimals(),
          'alias' => $stat->getExpressionAlias(),
        ];
      }
    }

    return $this->statMetaData;
  }

  /**
   * {@inheritdoc}
   */
  public function executeQuery() {

    $query = $this->getDatabase()->select($this->table, 'sl');

    foreach ($this->getStatMetaData() as $field => $data) {
      $alias = !empty($data['alias']) ? $data['alias'] : $field;
      $query->addExpression($data['expression'], $alias);
    }

    if (!empty($this->modifiers['groups'])) {
      foreach ($this->modifiers['groups'] as $group) {
        $query->groupBy($group);
      }
    }

    if (!empty($this->modifiers['filters'])) {
      foreach ($this->modifiers['filters'] as $filter) {
        $query->condition($filter['column'], $filter['value'], $filter['operator'] ?: '=');
      }
    }

    if (!empty($this->modifiers['sorts'])) {
      foreach ($this->modifiers['sorts'] as $sort) {
        $query->orderBy($sort['column'], $sort['direction'] ?: 'ASC');
      }
    }

    $table_sort = $query->extend('Drupal\Core\Database\Query\TableSortExtender')
      ->orderByHeader($this->getHeader());
    $pager = $table_sort->extend('Drupal\Core\Database\Query\PagerSelectExtender')
      ->limit(200);

    $this->results = $pager->execute()->fetchAll();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function addModifier($type, $modifier) {
    if (!isset($this->modifiers[$type])) {
      throw new \Exception(sprintf('Invalid modifier %s', $type));
    }

    $this->modifiers[$type][] = $modifier;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function setModifier(string $type, array $modifiers) {
    if (!isset($this->modifiers[$type])) {
      throw new \Exception(sprintf('Invalid modifier %s', $type));
    }

    foreach ($modifiers as $modifier) {
      $this->addModifier($type, $modifier);
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function setModifiers(array $modifiers) {
    foreach ($this->modifiers as $type => $current) {
      if (isset($modifiers[$type])) {
        if (!is_array($modifiers[$type])) {
          $this->addModifier($type, $modifiers[$type]);
        }
        else {
          $this->setModifier($type, $modifiers[$type]);
        }
      }
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function setStatValues(array $stat_values) {
    $stats = $this->getStats();
    foreach ($stat_values as $id => $value) {
      if (empty($stats[$id])) {
        throw \Exception(sprintf('Stat %s does not exist on stat line.', $id));
      }
      $stats[$id]->setValue($value);
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function compute() {
    $results = [];
    foreach ($this->getStats() as $id => $stat) {
      $results[$id] = $stat->compute();
    }
    $this->results[] = (object) $results;
    return $this;
  }
}

Here’s a StatLine type class for hitting-players:

<?php

namespace Drupal\my_module\Plugin\StatLine;

use Drupal\my_module\Plugin\StatLine\StatLinePluginBase;

/**
 * Provides stat line for players that hit.
 *
 * @StatLine(
 *   id = "hitting_players",
 *   name = @Translation("Hitting Players"),
 * )
 */
class HittingPlayers extends StatLinePluginBase {
  protected $table = 'hitting_players';
  protected $defaultSort = 'total';
  protected $statOrder = [
    'name',
    'year',
    'teams',
    'positions',
    'rank',
    'position_rank',
    'games',
    'hits',
    'at_bats',
    'runs',
    'home_runs',
    'rbis',
    'stolen_bases',
    'batting_average',
    'power_speed',
    'runs_total',
    'home_runs_total',
    'rbis_total',
    'stolen_bases_total',
    'batting_average_total',
    'total',
    'total_per_game',
  ];

  /**
   * {@inheritdoc}
   */
  public function setModifiers(array $modifiers) {
    $filters = [
      'name' => [],
      'year' => [],
      'positions' => [
        'operator' => 'LIKE',
      ],
      'games_min' => [
        'operator' => '>=',
        'column' => 'games',
      ],
      'games_max' => [
        'operator' => '<=',
        'column' => 'games',
      ],
    ];
    if (!empty($modifiers['filters'])) {
      foreach ($filters as $key => $filter) {
        if (!empty($modifiers['filters'][$key])) {
          $filter += [
            'column' => $key,
            'operator' => '=',
          ];
          $modifier = $modifiers['filters'][$key];
          unset($modifiers['filters'][$key]);
          $modifiers['filters'][] = [
            'column' => $filter['column'],
            'value' => $filter['operator'] == 'LIKE' ? '%' . $modifier . '%' : $modifier,
            'operator' => $filter['operator'],
          ];
        }
      }
    }
    return parent::setModifiers($modifiers);
  }
}

This is mostly a list of stats needed to build this statline. It also defines which table with which it’s associated and the query modifiers are a little different for this one than for the hitting-daily so it’s custom here. That’s not something we’re getting into detail about here either.

This is the entire Drupal plugin setup and it provides a way of retrieving and manipulating data from the underlying tables.

Visualization

From here, I added Drupal forms for modifying the queries (e.g. filtering by position, or grouping data by year/month). I also added a controller for providing the pages where this data can be viewed as an html table. These may end up in a later post but will be left out of this one.

In the end, I could retrieve all of the data from statline by running the following code:

// $modifiers can be set manually or can be pulled in from the request. It can be empty.
$stat_line = \Drupal::service('plugin.manager.stat_line')
               ->createInstance($stat_line_type);
$results = $stat_line->setModifiers($modifiers)
            ->executeQuery()
            ->results();