diff --git a/module/VuFindConsole/Module.php b/module/VuFindConsole/Module.php
index bbcbf4f931733ae4d14e43a50783c623719c3bcd..b0462b1990aad2d3610c1658f30929a5d936cd2e 100644
--- a/module/VuFindConsole/Module.php
+++ b/module/VuFindConsole/Module.php
@@ -104,6 +104,7 @@ class Module implements \Zend\ModuleManager\Feature\ConsoleUsageProviderInterfac
             'generate extendclass' => 'Subclass a service, w/ lookup by class name',
             'generate extendservice' => 'Override a service with a new child class',
             'generate nontabrecordaction' => 'Add routes for non-tab record action',
+            'generate plugin' => 'Create a new plugin class',
             'generate recordroute' => 'Add a record route',
             'generate staticroute' => 'Add a static route',
             'generate theme' => 'Create and configure a new theme',
diff --git a/module/VuFindConsole/config/module.config.php b/module/VuFindConsole/config/module.config.php
index bdadb5389c7d8d86c9fe590e9cffd7c5d693e39e..8289573f769a73ae58b772e816f306f99289fa60 100644
--- a/module/VuFindConsole/config/module.config.php
+++ b/module/VuFindConsole/config/module.config.php
@@ -55,6 +55,7 @@ $routes = [
     'generate/extendclass' => 'generate extendclass [--extendfactory] [<class>] [<target>]',
     'generate/extendservice' => 'generate extendservice [<source>] [<target>]',
     'generate/nontabrecordaction' => 'generate nontabrecordaction [<newAction>] [<module>]',
+    'generate/plugin' => 'generate plugin [<class>] [<factory>]',
     'generate/recordroute' => 'generate recordroute [<base>] [<newController>] [<module>]',
     'generate/staticroute' => 'generate staticroute [<name>] [<module>]',
     'generate/theme' => 'generate theme [<themename>]',
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
index 01252e446f5e7e0dd1a2122af42365468bb82b2f..aa190de726d9ddc2b7ec136a8d31f7df1215b6bf 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
@@ -222,6 +222,43 @@ class GenerateController extends AbstractBase
         return $this->getSuccessResponse();
     }
 
+    /**
+     * Create a new plugin class
+     *
+     * @return \Zend\Console\Response
+     */
+    public function pluginAction()
+    {
+        // Display help message if parameters missing:
+        $request = $this->getRequest();
+        $class = $request->getParam('class');
+        $factory = $request->getParam('factory');
+
+        if (empty($class)) {
+            Console::writeLine(
+                'Usage: ' . $request->getScriptName() . ' generate plugin'
+                . ' [class_name] [factory]'
+            );
+            Console::writeLine(
+                "\tclass_name - the name of the class you wish to create"
+            );
+            Console::writeLine(
+                "\tfactory - an existing factory to use (omit to generate a new one)"
+            );
+            return $this->getFailureResponse();
+        }
+
+        try {
+            $this->getGeneratorTools()
+                ->createPlugin($this->serviceLocator, $class, $factory);
+        } catch (\Exception $e) {
+            Console::writeLine($e->getMessage());
+            return $this->getFailureResponse();
+        }
+
+        return $this->getSuccessResponse();
+    }
+
     /**
      * Add a new record route definition
      *
diff --git a/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php b/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php
index 82732035c29dadd84ec704ecc7e43c342fca06e4..615112fb4e4268bdd5292b7afad994724c63715d 100644
--- a/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php
+++ b/module/VuFindConsole/src/VuFindConsole/Generator/GeneratorTools.php
@@ -62,6 +62,124 @@ class GeneratorTools
         $this->config = $config;
     }
 
+    /**
+     * Create a plugin class.
+     *
+     * @param ContainerInterface $container Service manager
+     * @param string             $class     Class name to create
+     * @param string             $factory   Existing factory to use (null to
+     * generate a new one)
+     *
+     * @return bool
+     * @throws \Exception
+     */
+    public function createPlugin(ContainerInterface $container, $class,
+        $factory = null
+    ) {
+        // Derive some key bits of information from the new class name:
+        $classParts = explode('\\', $class);
+        $module = $classParts[0];
+        $shortName = strtolower(array_pop($classParts));
+        $classParts[0] = 'VuFind';
+        $classParts[] = 'PluginManager';
+        $pmClass = implode('\\', $classParts);
+        // Set a flag for whether to generate a factory, and create class name
+        // if necessary. If existing factory specified, ensure it really exists.
+        if ($generateFactory = empty($factory)) {
+            $factory = $class . 'Factory';
+        } elseif (!class_exists($factory)) {
+            throw new \Exception("Undefined factory: $factory");
+        }
+
+        // Figure out further information based on the plugin manager:
+        if (!$container->has($pmClass)) {
+            throw new \Exception('Cannot find expected plugin manager: ' . $pmClass);
+        }
+        $pm = $container->get($pmClass);
+        if (!method_exists($pm, 'getExpectedInterface')) {
+            throw new \Exception(
+                $pmClass . ' does not implement getExpectedInterface!'
+            );
+        }
+
+        // Force getExpectedInterface() to be public so we can read it:
+        $reflectionMethod = new \ReflectionMethod($pm, 'getExpectedInterface');
+        $reflectionMethod->setAccessible(true);
+        $interface = $reflectionMethod->invoke($pm);
+
+        // Figure out whether the plugin requirement is an interface or a
+        // parent class so we can create the right thing....
+        if (interface_exists($interface)) {
+            $parent = null;
+            $interfaces = [$interface];
+        } else {
+            $parent = $interface;
+            $interfaces = [];
+        }
+        $apmFactory = new \VuFind\ServiceManager\AbstractPluginManagerFactory();
+        $pmKey = $apmFactory->getConfigKey(get_class($pm));
+        $configPath = ['vufind', 'plugin_managers', $pmKey];
+
+        // Generate the classes and configuration:
+        $this->createClassInModule($class, $module, $parent, $interfaces);
+        if ($generateFactory) {
+            $this->generateFactory($class, $factory, $module);
+        }
+        $factoryPath = array_merge($configPath, ['factories', $class]);
+        $this->writeNewConfig($factoryPath, $factory, $module);
+        $aliasPath = array_merge($configPath, ['aliases', $shortName]);
+        // Don't back up the config twice -- the first backup from the previous
+        // write operation is sufficient.
+        $this->writeNewConfig($aliasPath, $class, $module, false);
+
+        return true;
+    }
+
+    /**
+     * Generate a factory class.
+     *
+     * @param string $class   Name of class being built by factory
+     * @param string $factory Name of factory to generate
+     * @param string $module  Name of module to generate factory within
+     *
+     * @return void
+     */
+    protected function generateFactory($class, $factory, $module)
+    {
+        $this->createClassInModule(
+            $factory, $module, null,
+            ['Zend\ServiceManager\Factory\FactoryInterface'],
+            function ($generator) use ($class) {
+                $method = MethodGenerator::fromArray(
+                    [
+                        'name' => '__invoke',
+                        'body' => 'return new \\' . $class . '();',
+                    ]
+                );
+                $param1 = [
+                    'name' => 'container',
+                    'type' => 'Interop\Container\ContainerInterface'
+                ];
+                $param2 = [
+                    'name' => 'requestedName',
+                ];
+                $param3 = [
+                    'name' => 'options',
+                    'type' => 'array',
+                    'defaultValue' => null,
+                ];
+                $method->setParameters([$param1, $param2, $param3]);
+                // Copy doc block from this class' factory:
+                $reflection = new \Zend\Code\Reflection\MethodReflection(
+                    GeneratorToolsFactory::class, '__invoke'
+                );
+                $example = MethodGenerator::fromReflection($reflection);
+                $method->setDocBlock($example->getDocBlock());
+                $generator->addMethods([$method]);
+            }
+        );
+    }
+
     /**
      * Extend a class defined somewhere in the service manager or its child
      * plugin managers.
@@ -396,16 +514,22 @@ class GeneratorTools
      * Extend a specified class within a specified module. Return the name of
      * the new subclass.
      *
-     * @param string $class  Name of class to create
-     * @param string $module Module in which to create the new class
-     * @param string $parent Parent class (null for no parent)
+     * @param string    $class      Name of class to create
+     * @param string    $module     Module in which to create the new class
+     * @param string    $parent     Parent class (null for no parent)
+     * @param string[]  $interfaces Interfaces for class to implement
+     * @param \Callable $callback   Callback to set up class generator
      *
      * @return void
      * @throws \Exception
      */
-    protected function createClassInModule($class, $module, $parent = null)
-    {
-        $generator = new ClassGenerator($class, null, null, $parent);
+    protected function createClassInModule($class, $module, $parent = null,
+        array $interfaces = [], $callback = null
+    ) {
+        $generator = new ClassGenerator($class, null, null, $parent, $interfaces);
+        if (is_callable($callback)) {
+            $callback($generator);
+        }
         return $this->writeClass($generator, $module);
     }