diff --git a/includes/rules.core.inc b/includes/rules.core.inc
index de99e3d..668cfa5 100644
--- a/includes/rules.core.inc
+++ b/includes/rules.core.inc
@@ -1469,6 +1469,15 @@ abstract class RulesAbstractPlugin extends RulesPlugin {
       // @see self::forceSetup().
       $this->infoLoaded = TRUE;
 
+      // Register the defined class, if any.
+      if (isset($this->info['class'])) {
+        $this->faces['RulesPluginImplInterface'] = 'RulesPluginImplInterface';
+        $face_methods = get_class_methods('RulesPluginImplInterface');
+        $class_info = array(1 => $this->info['class']);
+        foreach ($face_methods as $method) {
+          $this->facesMethods[$method] = $class_info;
+        }
+      }
       // Add in per-plugin implementation callbacks if any.
       if (!empty($this->info['faces_cache'])) {
         foreach ($this->info['faces_cache'] as $face => $data) {
@@ -1690,7 +1699,15 @@ abstract class RulesAbstractPlugin extends RulesPlugin {
         'callbacks' => array(),
       );
       unset($info['arguments'], $info['new variables']);
-      $info['callbacks'] += array('execute' => $info['base']);
+
+      if (function_exists($info['base'])) {
+        $info['callbacks'] += array('execute' => $info['base']);
+      }
+
+      // We do not need to build a faces cache for RulesPluginHandlerInterface,
+      // which gets added in automatically as its a parent of
+      // RulesPluginImplInterface.
+      unset($this->faces['RulesPluginHandlerInterface']);
 
       // Build up the per plugin implementation faces cache.
       foreach ($this->faces as $interface) {
@@ -1702,6 +1719,11 @@ abstract class RulesAbstractPlugin extends RulesPlugin {
             $methods[$method][0] = $function;
             $file_names[$method] = $this->getFileName($function, $includes);
           }
+          // Note that this skips RulesPluginImplInterface, which is not
+          // implemented by plugin handlers.
+          elseif (isset($info['class']) && is_subclass_of($info['class'], $interface)) {
+            $methods[$method][1] = $info['class'];
+          }
           elseif (function_exists($function = $info['base'] . '_' . $method)) {
             $methods[$method][0] = $function;
             $file_names[$method] = $this->getFileName($function, $includes);
@@ -1734,7 +1756,7 @@ abstract class RulesAbstractPlugin extends RulesPlugin {
   /**
    * Make sure all supported destinations are included.
    */
-  protected static function includeFiles() {
+  public static function includeFiles() {
     static $included;
 
     if (!isset($included)) {
@@ -1744,6 +1766,20 @@ abstract class RulesAbstractPlugin extends RulesPlugin {
           module_load_include('inc', $module, $name);
         }
       }
+      $dirs = array();
+      foreach (module_implements('rules_directory') as $module) {
+        // Include all files once, so the discovery can find them.
+        $result = module_invoke($module, 'rules_directory');
+        if (!is_array($result)) {
+          $result = array($module => $result);
+        }
+        $dirs += $result;
+      }
+      foreach ($dirs as $module => $directory) {
+        foreach (glob(drupal_get_path('module', $module) . "/$directory/*.{inc,php}", GLOB_BRACE) as $filename) {
+          include_once $filename;
+        }
+      }
       $included = TRUE;
     }
   }
@@ -1858,15 +1894,9 @@ interface RulesTriggerableInterface {
 }
 
 /**
- * Provides the interface used for implementing an abstract plugin by using
- * the Faces extension mechanism.
+ * Provides the base interface for implementing abstract plugins via classes.
  */
-interface RulesPluginImplInterface {
-
-  /**
-   * Execute the action or condition making use of the parameters as specified.
-   */
-  public function execute();
+interface RulesPluginHandlerInterface {
 
   /**
    * Validates $settings independent from a form submission.
@@ -1933,6 +1963,49 @@ interface RulesPluginImplInterface {
 }
 
 /**
+ * Interface for implementing conditions via classes.
+ *
+ * In addition to the interface an execute() and a static getInfo() method must
+ * be implemented. The static getInfo() method has to return the info as
+ * returned by hook_rules_condition_info() but including an additional 'name'
+ * key, specifying the plugin name.
+ * The execute method is the equivalent to the usual execution callback and
+ * gets the parameters passed as specified in the info array.
+ *
+ * See RulesNodeConditionType for an example and rules_discover_plugins()
+ * for information about class discovery.
+ */
+interface RulesConditionHandlerInterface extends RulesPluginHandlerInterface {}
+
+/**
+ * Interface for implementing actions via classes.
+ *
+ * In addition to the interface an execute() and a static getInfo() method must
+ * be implemented. The static getInfo() method has to return the info as
+ * returned by hook_rules_action_info() but including an additional 'name' key,
+ * specifying the plugin name.
+ * The execute method is the equivalent to the usual execution callback and
+ * gets the parameters passed as specified in the info array.
+ *
+ * See RulesNodeConditionType for an example and rules_discover_plugins()
+ * for information about class discovery.
+ */
+interface RulesActionHandlerInterface extends RulesPluginHandlerInterface {}
+
+/**
+ *
+ * Provides the interface used for implementing an abstract plugin by using
+ * the Faces extension mechanism.
+ */
+interface RulesPluginImplInterface extends RulesPluginHandlerInterface {
+
+  /**
+   * Execute the action or condition making use of the parameters as specified.
+   */
+  public function execute();
+}
+
+/**
  * Interface for optimizing evaluation.
  *
  * @see RulesContainerPlugin::optimize()
@@ -1945,18 +2018,21 @@ interface RulesOptimizationInterface {
 }
 
 /**
- * Class providing default implementations of the methods of the RulesPluginImplInterface.
- *
- * If a plugin implementation does not provide a function for a method, the
- * default method of this class will be invoked.
- *
- * @see RulesPluginImplInterface
- * @see RulesAbstractPlugin
+ * Base class for implementing abstract plugins via classes.
  */
-class RulesAbstractPluginDefaults extends FacesExtender implements RulesPluginImplInterface {
+abstract class RulesPluginHandlerBase extends FacesExtender implements RulesPluginHandlerInterface {
 
-  public function execute() {
-    throw new RulesEvaluationException($this->object->getPluginName() .": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR);
+  /**
+   * @var RulesAbstractPlugin
+   */
+  protected $element;
+
+  /**
+   * Overridden to provide $this->element to make the code more meaningful.
+   */
+  public function __construct(FacesExtendable $object) {
+    $this->object = $object;
+    $this->element = $object;
   }
 
   /**
@@ -1975,6 +2051,32 @@ class RulesAbstractPluginDefaults extends FacesExtender implements RulesPluginIm
 }
 
 /**
+ * Base class for implementing conditions via classes.
+ */
+abstract class RulesConditionHandlerBase extends RulesPluginHandlerBase implements RulesConditionHandlerInterface {}
+
+/**
+ * Base class for implementing actions via classes.
+ */
+abstract class RulesActionHandlerBase extends RulesPluginHandlerBase implements RulesActionHandlerInterface {}
+
+/**
+ * Class providing default implementations of the methods of the RulesPluginImplInterface.
+ *
+ * If a plugin implementation does not provide a function for a method, the
+ * default method of this class will be invoked.
+ *
+ * @see RulesPluginImplInterface
+ * @see RulesAbstractPlugin
+ */
+class RulesAbstractPluginDefaults extends RulesPluginHandlerBase implements RulesPluginImplInterface {
+
+  public function execute() {
+    throw new RulesEvaluationException($this->object->getPluginName() .": Execution implementation is missing.", array(), $this->object, RulesLog::ERROR);
+  }
+}
+
+/**
  * A RecursiveIterator for rule elements.
  */
 class RulesRecursiveElementIterator extends ArrayIterator implements RecursiveIterator {
diff --git a/includes/rules.event.inc b/includes/rules.event.inc
index 8302f09..27f6a33 100755
--- a/includes/rules.event.inc
+++ b/includes/rules.event.inc
@@ -28,6 +28,10 @@
  * rules_invoke_event('node_view', $node, $view_mode);
  * @endcode
  *
+ * Rules event handlers have to be declared using the 'class' key in
+ * hook_rules_event_info(), or may be discovered automatically, see
+ * rules_discover_plugins() for details.
+ *
  * @see RulesEventHandlerBase
  * @see RulesEventDefaultHandler
  */
diff --git a/modules/node.eval.inc b/modules/node.eval.inc
index ee96e15..39922b0 100644
--- a/modules/node.eval.inc
+++ b/modules/node.eval.inc
@@ -9,31 +9,127 @@
  */
 
 /**
+ * Base class providing node condition defaults.
+ */
+abstract class RulesNodeConditionBase extends RulesConditionHandlerBase {
+
+  public static function defaults() {
+    return array(
+      'parameter' => array(
+        'node' => array('type' => 'node', 'label' => t('Content')),
+      ),
+      'group' => t('Node'),
+      'access callback' => 'rules_node_integration_access',
+    );
+  }
+}
+
+/**
  * Condition: Check for selected content types
  */
-function rules_condition_node_is_of_type($node, $types) {
-  return in_array($node->type, $types);
+class RulesNodeConditionType extends RulesNodeConditionBase {
+
+  /**
+   * Defines the condition.
+   */
+  public static function getInfo() {
+    $info = self::defaults() + array(
+      'name' => 'node_is_of_type',
+      'label' => t('Content is of type'),
+      'help' => t('Evaluates to TRUE if the given content is of one of the selected content types.'),
+    );
+    $info['parameter']['type'] = array(
+      'type' => 'list<text>',
+      'label' => t('Content types'),
+      'options list' => 'node_type_get_names',
+      'description' => t('The content type(s) to check for.'),
+      'restriction' => 'input',
+    );
+    return $info;
+  }
+
+  /**
+   * Executes the condition.
+   */
+  public function execute($node, $types) {
+    return in_array($node->type, $types);
+  }
+
+  /**
+   * Provides the content type of a node as asserted metadata.
+   */
+  function assertions() {
+    return array('node' => array('bundle' => $this->element->settings['type']));
+  }
 }
 
 /**
- * Condition: Check if the node is published
+ * Condition: Check if the node is published.
  */
-function rules_condition_node_is_published($node) {
-  return $node->status == 1;
+class RulesNodeConditionPublished extends RulesNodeConditionBase {
+
+  /**
+   * Defines the condition.
+   */
+  public static function getInfo() {
+    return self::defaults() + array(
+      'name' => 'node_is_published',
+      'label' => t('Content is published'),
+    );
+  }
+
+  /**
+   * Executes the condition.
+   */
+  public function execute($node) {
+    return $node->status == 1;
+  }
 }
 
 /**
- * Condition: Check if the node is sticky
+ * Condition: Check if the node is sticky.
  */
-function rules_condition_node_is_sticky($node) {
-  return $node->sticky == 1;
+class RulesNodeConditionSticky extends RulesNodeConditionBase {
+
+  /**
+   * Defines the condition.
+   */
+  public static function getInfo() {
+    return self::defaults() + array(
+      'name' => 'node_is_sticky',
+      'label' => t('Content is sticky'),
+    );
+  }
+
+  /**
+   * Executes the condition.
+   */
+  public function execute($node) {
+    return $node->sticky == 1;
+  }
 }
 
 /**
  * Condition: Check if the node is promoted to the frontpage
  */
-function rules_condition_node_is_promoted($node) {
-  return $node->promote == 1;
+class RulesNodeConditionPromoted extends RulesNodeConditionBase {
+
+  /**
+   * Defines the condition.
+   */
+  public static function getInfo() {
+    return self::defaults() + array(
+      'name' => 'node_is_promoted',
+      'label' => t('Content is promoted to frontpage'),
+    );
+  }
+
+  /**
+   * Executes the condition.
+   */
+  public function execute($node) {
+    return $node->promote == 1;
+  }
 }
 
 /**
diff --git a/modules/node.rules.inc b/modules/node.rules.inc
index 9fea011..a126931 100644
--- a/modules/node.rules.inc
+++ b/modules/node.rules.inc
@@ -89,51 +89,6 @@ function rules_events_node_variables($node_label, $update = FALSE) {
 }
 
 /**
- * Implements hook_rules_condition_info() on behalf of the node module.
- */
-function rules_node_condition_info() {
-  $defaults = array(
-    'parameter' => array(
-      'node' => array('type' => 'node', 'label' => t('Content')),
-    ),
-    'group' => t('Node'),
-    'access callback' => 'rules_node_integration_access',
-  );
-  $items['node_is_of_type'] = $defaults + array(
-    'label' => t('Content is of type'),
-    'help' => t('Evaluates to TRUE if the given content is of one of the selected content types.'),
-    'base' => 'rules_condition_node_is_of_type',
-  );
-  $items['node_is_of_type']['parameter']['type'] = array(
-    'type' => 'list<text>',
-    'label' => t('Content types'),
-    'options list' => 'node_type_get_names',
-    'description' => t('The content type(s) to check for.'),
-    'restriction' => 'input',
-  );
-  $items['node_is_published'] = $defaults + array(
-    'label' => t('Content is published'),
-    'base' => 'rules_condition_node_is_published',
-  );
-  $items['node_is_sticky'] = $defaults + array(
-    'label' => t('Content is sticky'),
-    'base' => 'rules_condition_node_is_sticky',
-  );
-  $items['node_is_promoted'] = $defaults + array(
-    'label' => t('Content is promoted to frontpage'),
-    'base' => 'rules_condition_node_is_promoted',
-  );
-  return $items;
-}
-
-/**
- * Provides the content type of a node as asserted metadata.
- */
-function rules_condition_node_is_of_type_assertions($element) {
-  return array('node' => array('bundle' => $element->settings['type']));
-}
-
-/**
  * Implements hook_rules_action_info() on behalf of the node module.
  */
 function rules_node_action_info() {
diff --git a/rules.api.php b/rules.api.php
index e672117..c9a2524 100644
--- a/rules.api.php
+++ b/rules.api.php
@@ -30,6 +30,10 @@
  * placed into the file MODULENAME.rules.inc, which gets automatically included
  * when the hook is invoked.
  *
+ * However, as an alternative to implementing this hook, class based plugin
+ * handlers may be provided by implementing RulesActionHandlerInterface. See
+ * the interface for details.
+ *
  * @return
  *   An array of information about the module's provided rules actions.
  *   The array contains a sub-array for each action, with the action name as
@@ -171,6 +175,27 @@ function hook_rules_file_info() {
 }
 
 /**
+ * Specifies directories for class-based plugin handler discovery.
+ *
+ * Implementing this hook is not a requirement, it is just one option to load
+ * the files containing the classes during discovery - see
+ * rules_discover_plugins().
+ *
+ * @return string|array
+ *   A directory relative to the module directory, which holds the files
+ *   containing rules plugin handlers, or multiple directories keyed by the
+ *   module the directory is contained in.
+ *   All files in those directories having a 'php' or 'inc' file extension will
+ *   be loaded during discovery. Optionally, wildcards ('*') may be used to
+ *   match multiple directories.
+ *
+ * @see rules_discover_plugins()
+ */
+function hook_rules_directory() {
+  return 'lib/Drupal/fluxtwitter/Rules/*';
+}
+
+/**
  * The execution callback for an action.
  *
  * It should be placed in any file included by your module or in a file
@@ -203,6 +228,10 @@ function rules_action_execution_callback($node, $title, $settings) {
  * placed into the file MODULENAME.rules.inc, which gets automatically included
  * when the hook is invoked.
  *
+ * However, as an alternative to implementing this hook, class based plugin
+ * handlers may be provided by implementing RulesConditionHandlerInterface. See
+ * the interface for details.
+ *
  * Adding conditions works exactly the same way as adding actions, with the
  * exception that conditions can't provide variables and cannot save parameters.
  * Thus the 'provides' attribute is not supported. Furthermore the condition
@@ -234,6 +263,10 @@ function hook_rules_condition_info() {
  * usually it's invoked directly from the providing module but wrapped by a
  * module_exists('rules') check.
  *
+ * However, as an alternative to implementing this hook, class based event
+ * handlers may be provided by implementing RulesEventHandlerInterface. See
+ * the interface for details.
+ *
  * @return
  *   An array of information about the module's provided rules events. The array
  *   contains a sub-array for each event, with the event name as the key. The
diff --git a/rules.info b/rules.info
index d7ab7fe..e80769d 100644
--- a/rules.info
+++ b/rules.info
@@ -11,6 +11,7 @@ files[] = includes/rules.processor.inc
 files[] = includes/rules.plugins.inc
 files[] = includes/rules.state.inc
 files[] = includes/rules.dispatcher.inc
+files[] = modules/node.eval.inc
 files[] = modules/php.eval.inc
 files[] = modules/rules_core.eval.inc
 files[] = modules/system.eval.inc
diff --git a/rules.module b/rules.module
index 97f4c71..def7c06 100644
--- a/rules.module
+++ b/rules.module
@@ -206,6 +206,11 @@ function rules_log($msg, $args = array(), $priority = RulesLog::INFO, RulesPlugi
  */
 function rules_fetch_data($hook) {
   $data = &drupal_static(__FUNCTION__, array());
+  static $discover = array(
+    'action_info' => 'RulesActionHandlerInterface',
+    'condition_info' => 'RulesConditionHandlerInterface',
+    'event_info' => 'RulesEventHandlerInterface',
+  );
 
   if (!isset($data[$hook])) {
     foreach (module_implements('rules_' . $hook) as $module) {
@@ -217,12 +222,78 @@ function rules_fetch_data($hook) {
         }
       }
     }
+    // Support class discovery.
+    if (isset($discover[$hook])) {
+      $data[$hook] += rules_discover_plugins($discover[$hook]);
+    }
     drupal_alter('rules_'. $hook, $data[$hook]);
   }
   return $data[$hook];
 }
 
 /**
+ * Discover plugin implementations.
+ *
+ * Class based plugin handlers must be loaded when rules caches are rebuilt,
+ * such that they get discovered properly. You have the following options:
+ *  - Put it into a regular module file (discouraged)
+ *  - Put it into your module.rules.inc file
+ *  - Put it in any file and declare it using hook_rules_file_info()
+ *  - Put it in any file and declare it using hook_rules_directory()
+ *
+ * In addition to that, the class must be loadable via regular class
+ * auto-loading, thus put the file holding the class in your info file or use
+ * another class-loader.
+ *
+ * @param string $class
+ *   The class or interface the plugins must implement. For a plugin to be
+ *   discovered it must have a static getInfo() method also.
+ *
+ * @return array
+ *   An info-hook style array containing info about discovered plugins.
+ *
+ * @see RulesActionHandlerInterface
+ * @see RulesConditionHandlerInterface
+ * @see RulesEventHandlerInterface
+ */
+function rules_discover_plugins($class) {
+  // Make sure all files possibly holding plugins are included.
+  RulesAbstractPlugin::includeFiles();
+
+  $items = array();
+  foreach (get_declared_classes() as $plugin_class) {
+    if (is_subclass_of($plugin_class, $class) && method_exists($plugin_class, 'getInfo')) {
+      $info = call_user_func(array($plugin_class, 'getInfo'));
+      $info['class'] = $plugin_class;
+      $info['module'] = _rules_discover_module($plugin_class);
+      $items[$info['name']] = $info;
+    }
+  }
+  return $items;
+}
+
+/**
+ * Determines the module providing the given class.
+ */
+function _rules_discover_module($class) {
+  $paths = &drupal_static(__FUNCTION__);
+
+  if (!isset($paths)) {
+    // Build up a map of modules keyed by their directory.
+    foreach (system_list('module_enabled') as $name => $module_info) {
+      $paths[dirname($module_info->filename)] = $name;
+    }
+  }
+  // Go up the path until we match a module up.
+  $reflection = new ReflectionClass($class);
+  $file_path = str_replace(DRUPAL_ROOT . '/', '', $reflection->getFileName());
+  while (!isset($paths[$file_path]) && strpos($file_path, DIRECTORY_SEPARATOR) !== FALSE) {
+    $file_path = dirname($file_path);
+  }
+  return isset($paths[$file_path]) ? $paths[$file_path] : FALSE;
+}
+
+/**
  * Gets a rules cache entry.
  */
 function &rules_get_cache($cid = 'data') {
@@ -688,7 +759,7 @@ function rules_entity_info() {
  * Implementation of hook_hook_info().
  */
 function rules_hook_info() {
-  foreach(array('plugin_info', 'data_info', 'condition_info', 'action_info', 'event_info', 'file_info', 'evaluator_info', 'data_processor_info') as $hook) {
+  foreach(array('plugin_info', 'rules_directory', 'data_info', 'condition_info', 'action_info', 'event_info', 'file_info', 'evaluator_info', 'data_processor_info') as $hook) {
     $hooks['rules_' . $hook] = array(
       'group' => 'rules',
     );
