From ad6c5ae443e139a2fddaef5c5e69444575b0050b Mon Sep 17 00:00:00 2001
From: Aaron Bauman <aaron@messageagency.com>
Date: Thu, 9 Aug 2018 17:37:05 -0400
Subject: [PATCH] Issue #2975835: Compatibility with drush 9

Initial pass at updating to drush 9 compatibility updates salesforce.drush functions only.
Still todo: salesforce_pull, salesforce_push, salesforce_mapping.
These should be faster now that the framework is in place.
---
 composer.json                       |  45 +-
 drush.services.yml                  |   6 +
 salesforce.drush.inc                |  36 ++
 src/Commands/SalesforceCommands.php | 613 ++++++++++++++++++++++++++++
 4 files changed, 681 insertions(+), 19 deletions(-)
 create mode 100644 drush.services.yml
 create mode 100644 src/Commands/SalesforceCommands.php

diff --git a/composer.json b/composer.json
index 3ad8d42a..e01e804d 100644
--- a/composer.json
+++ b/composer.json
@@ -1,22 +1,29 @@
 {
-  "name": "drupal/salesforce",
-  "description": "Provides Drupal modules to integrate with Salesforce.",
-  "type": "drupal-module",
-  "homepage": "https://drupal.org/project/salesforce",
-  "authors": [
-    {
-      "name": "Aaron Bauman (aaronbauman)",
-      "homepage": "https://www.drupal.org/u/aaronbauman",
-      "role": "Maintainer"
+    "name": "drupal/salesforce",
+    "description": "Provides Drupal modules to integrate with Salesforce.",
+    "type": "drupal-module",
+    "homepage": "https://drupal.org/project/salesforce",
+    "authors": [
+        {
+            "name": "Aaron Bauman (aaronbauman)",
+            "homepage": "https://www.drupal.org/u/aaronbauman",
+            "role": "Maintainer"
+        },
+        {
+            "name": "Alexander Rhodes (ironsizide)",
+            "homepage": "https://www.drupal.org/u/ironsizide",
+            "role": "Maintainer"
+        }
+    ],
+    "support": {
+        "issues": "https://drupal.org/project/issues/salesforce",
+        "source": "http://cgit.drupalcode.org/salesforce"
     },
-    {
-      "name": "Alexander Rhodes (ironsizide)",
-      "homepage": "https://www.drupal.org/u/ironsizide",
-      "role": "Maintainer"
+    "extra": {
+        "drush": {
+            "services": {
+                "drush.services.yml": "^9"
+            }
+        }
     }
-  ],
-  "support": {
-    "issues": "https://drupal.org/project/issues/salesforce",
-    "source": "http://cgit.drupalcode.org/salesforce"
-  }
-}
+}
\ No newline at end of file
diff --git a/drush.services.yml b/drush.services.yml
new file mode 100644
index 00000000..c671c4c5
--- /dev/null
+++ b/drush.services.yml
@@ -0,0 +1,6 @@
+services:
+  salesforce.commands:
+    class: \Drupal\salesforce\Commands\SalesforceCommands
+    arguments: ['@salesforce.client']
+    tags:
+      - { name: drush.command }
diff --git a/salesforce.drush.inc b/salesforce.drush.inc
index 03e569c8..fe763969 100644
--- a/salesforce.drush.inc
+++ b/salesforce.drush.inc
@@ -10,6 +10,8 @@ use Drupal\salesforce\SelectQuery;
 
 /**
  * Implements hook_drush_command().
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function salesforce_drush_command() {
   $items['sf-rest-version'] = [
@@ -146,8 +148,11 @@ raw: Display the complete, raw describe response.",
  * List the resources available for the specified API version.
  *
  * This command provides the name and URI of each resource.
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_list_resources() {
+  _drush_salesforce_deprecated();
   $salesforce = \Drupal::service('salesforce.client');
   $resources = $salesforce->listResources();
   if ($resources) {
@@ -172,8 +177,12 @@ function drush_salesforce_sf_list_resources() {
  *
  * @param string $object_name
  *   The name of a Salesforce object to query.
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_describe_object($object_name = NULL) {
+  _drush_salesforce_deprecated();
+
   if (!$object_name) {
     return drush_log('Please specify an object as an argument.', 'error');
   }
@@ -249,8 +258,12 @@ function drush_salesforce_sf_describe_object($object_name = NULL) {
 
 /**
  * Displays information about the REST API version the site is using.
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_rest_version() {
+  _drush_salesforce_deprecated();
+
   $salesforce = \Drupal::service('salesforce.client');
   $version_id = $salesforce->getApiVersion();
   $versions = $salesforce->getVersions();
@@ -270,10 +283,14 @@ function drush_salesforce_sf_rest_version() {
  *
  * This command lists Salesforce objects that are available in your organization
  * and available to the logged-in user.
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_list_objects() {
+  _drush_salesforce_deprecated();
   $salesforce = \Drupal::service('salesforce.client');
   if ($objects = $salesforce->objects()) {
+    print_r($objects);
     drush_print('The following objects are available in your organization and available to the logged-in user.');
     $rows[] = ['Name', 'Label', 'Label Plural'];
     foreach ($objects as $object) {
@@ -298,8 +315,11 @@ function drush_salesforce_sf_list_objects() {
  *   The object type name, e.g. Account
  * @param $id
  *   The Salesforce ID
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_read_object($id) {
+  _drush_salesforce_deprecated();
   $salesforce = \Drupal::service('salesforce.client');
   try {
     $name = $salesforce->getObjectTypeName(new SFID($id));
@@ -323,8 +343,11 @@ function drush_salesforce_sf_read_object($id) {
  *   The object type name, e.g. Account
  * @param $data
  *   The object data, or '-' to read from stdin
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_create_object($name, $data) {
+  _drush_salesforce_deprecated();
 
   if ($data == '-') {
     $data = stream_get_contents(STDIN);
@@ -360,8 +383,11 @@ function drush_salesforce_sf_create_object($name, $data) {
  *
  * @param $name
  *   The object type name, e.g. Account
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_query_object($name) {
+  _drush_salesforce_deprecated();
   $salesforce = \Drupal::service('salesforce.client');
 
   $query = new SelectQuery($name);
@@ -419,8 +445,11 @@ function drush_salesforce_sf_query_object($name) {
  *
  * @param $query
  *   The query to execute
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function drush_salesforce_sf_execute_query($query = NULL) {
+  _drush_salesforce_deprecated();
   if (!$query) {
     return drush_log('Please specify a query as an argument.', 'error');
   }
@@ -441,8 +470,11 @@ function drush_salesforce_sf_execute_query($query = NULL) {
  * @param string $name
  *
  * @return SalesforceMappingInterface
+ *
+ * @deprecated Support for drush 8 is deprecated and will be removed in a future release.
  */
 function _salesforce_drush_get_mapping($name = NULL) {
+  _drush_salesforce_deprecated();
   $mapping_storage = \Drupal::service('entity_type.manager')
     ->getStorage('salesforce_mapping');
 
@@ -465,3 +497,7 @@ function _salesforce_drush_get_mapping($name = NULL) {
   }
   return $mapping;
 }
+
+function _drush_salesforce_deprecated() {
+  trigger_error('Salesforce module support for Drush 8 is deprecated and will be removed in a future release', E_DEPRECATED);
+}
diff --git a/src/Commands/SalesforceCommands.php b/src/Commands/SalesforceCommands.php
new file mode 100644
index 00000000..7e9f43be
--- /dev/null
+++ b/src/Commands/SalesforceCommands.php
@@ -0,0 +1,613 @@
+<?php
+
+namespace Drupal\salesforce\Commands;
+
+use Consolidation\OutputFormatters\Formatters\TableFormatter;
+use Consolidation\OutputFormatters\Formatters\VarDumpFormatter;
+use Consolidation\OutputFormatters\Options\FormatterOptions;
+use Consolidation\OutputFormatters\StructuredData\PropertyList;
+use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
+use Consolidation\OutputFormatters\StructuredData\RowsOfFieldsWithMetadata;
+use Drupal\salesforce\Rest\RestClient;
+use Drupal\salesforce\SelectQuery;
+use Drupal\salesforce\SFID;
+use Drush\Commands\DrushCommands;
+use Drush\Exceptions\UserAbortException;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Helper\TableCell;
+use Symfony\Component\Console\Input\Input;
+use Symfony\Component\Console\Output\Output;
+use Symfony\Component\Translation\Util\ArrayConverter;
+
+/**
+ * A Drush commandfile.
+ *
+ * In addition to this file, you need a drush.services.yml
+ * in root of your module, and a composer.json file that provides the name
+ * of the services file to use.
+ *
+ * See these files for an example of injecting Drupal services:
+ *   - http://cgit.drupalcode.org/devel/tree/src/Commands/DevelCommands.php
+ *   - http://cgit.drupalcode.org/devel/tree/drush.services.yml
+ */
+class SalesforceCommands extends DrushCommands {
+
+  /** @var \Drupal\salesforce\Rest\RestClient */
+  protected $client;
+
+  public function __construct(RestClient $client) {
+    $this->client = $client;
+  }
+
+  /**
+   * Display information about the current REST API version.
+   *
+   * @command salesforce:rest-version
+   * @aliases sfrv,sf-rest-version
+   * @field-labels
+   *   label: Label
+   *   url: Path
+   *   version: Version
+   *   login_url: Login URL
+   *   latest: Latest Version?
+   * @default-fields label,url,version,login_url,latest
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\PropertyList
+   */
+  public function restVersion() {
+    $version_id = $this->client->getApiVersion();
+    $versions = $this->client->getVersions();
+    $version = $versions[$version_id];
+    $latest = array_pop($versions);
+    foreach ($version as $key => $value) {
+      $rows[$key] = $value;
+    }
+    $rows['login_url'] = $this->client->getLoginUrl();
+    $rows['latest'] = strcmp($version_id, $latest['version']) ? $latest['version'] : 'Yes';
+    return new PropertyList($rows);
+  }
+
+  /**
+   * List the objects that are available in your organization and available to the logged-in user.
+   *
+   * @command salesforce:list-objects
+   * @aliases sflo,sf-list-objects
+   * @field-labels
+   *   activateable: Activateable
+   *   createable: Createable
+   *   custom: Custom
+   *   customSetting: CustomSetting
+   *   deletable: Deletable
+   *   deprecatedAndHidden: DeprecatedAndHidden
+   *   feedEnabled: FeedEnabled
+   *   hasSubtypes: HasSubtypes
+   *   isSubtype: IsSubtype
+   *   keyPrefix: KeyPrefix
+   *   label: Label
+   *   labelPlural: LabelPlural
+   *   layoutable: Layoutable
+   *   mergeable: Mergeable
+   *   mruEnabled: MruEnabled
+   *   name: Name
+   *   queryable: Queryable
+   *   replicateable: Replicateable
+   *   retrieveable: Retrieveable
+   *   searchable: Searchable
+   *   triggerable: Triggerable
+   *   undeletable: Undeletable
+   *   updateable: Updateable
+   *   urls: URLs
+   * @default-fields name,label,labelPlural
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   */
+  public function listObjects() {
+    if ($objects = $this->client->objects()) {
+      foreach ($objects as $name => $object) {
+        $rows[$name] = $object;
+        $rows[$name]['urls'] = new TableCell(implode("\n", $rows[$name]['urls']) . "\n");
+      }
+      return new RowsOfFields($rows);
+    }
+    throw new \Exception('Could not load any information about available objects.');
+  }
+
+  /**
+   * @hook interact salesforce:describe-object
+   */
+  public function interactDescribeObject(Input $input, Output $output) {
+    return $this->interactObject($input, $output);
+  }
+
+  /**
+   * @hook interact salesforce:describe-fields
+   */
+  public function interactDescribeFields(Input $input, Output $output) {
+    return $this->interactObject($input, $output);
+  }
+
+  /**
+   * @hook interact salesforce:describe-metadata
+   */
+  public function interactDescribeMetadata(Input $input, Output $output) {
+    return $this->interactObject($input, $output);
+  }
+
+  /**
+   * @hook interact salesforce:describe-record-types
+   */
+  public function interactDescribeRecordTypes(Input $input, Output $output) {
+    return $this->interactObject($input, $output);
+  }
+
+  /**
+   * @hook interact salesforce:dump-object
+   */
+  public function interactDumpObject(Input $input, Output $output) {
+    return $this->interactObject($input, $output);
+  }
+
+  /**
+   * If there's a way to attach multiple hooks to one method, please do it here!
+   */
+  protected function interactObject(Input $input, Output $output, $message = 'Enter a Salesforce object to describe') {
+    if (!$input->getArgument('object')) {
+      if (!$answer = $this->io()->ask($message)) {
+        throw new UserAbortException();
+      }
+      $input->setArgument('object', $answer);
+    }
+  }
+
+  /**
+   * Retrieve all the metadata for an object, including information about each field, URLs, and child relationships.
+   *
+   * @param $object
+   *   The object name in Salesforce.
+    * @param array $options An associative array of options whose values come from cli, aliases, config, etc.
+   * @option output
+   *   Specify an output type.
+   *   Options are:
+   *     info: (default) Display metadata about an object
+   *     fields: Display information about fields that are part of the object
+   *     field: Display information about a specific field that is part of an object
+   *     raw: Display the complete, raw describe response.
+   * @option field
+   *   For "field" output type, specify a fieldname.
+   * @usage drush sfdo Contact
+   *   Show metadata about Contact SObject type.
+   * @usage drush sfdo Contact --output=fields
+   *   Show addtional metadata about Contact fields.
+   * @usage drush sfdo Contact --output=field --field=Email
+   *   Show full metadata about Contact.Email field.
+   * @usage drush sfdo Contact --output=raw
+   *   Display the full metadata for Contact SObject type.
+   *
+   * @command salesforce:describe-object-deprecated
+   * @deprecated Use describeFields, describeMetadata, describeRecordTypes, dumpObject
+   */
+  public function describeObject($object, array $options = ['output' => null, 'field' => null]) {
+    return $this->describeFields($object);
+  }
+
+  /**
+   * Dump the raw describe response for given object.
+   *
+   * @command salesforce:dump-object
+   * @aliases sf-dump-object
+   */
+  public function dumpObject($object) {
+    $objectDescription = $this->client->objectDescribe($object);
+    if (!is_object($objectDescription)) {
+      throw new \Exception(dt('Could not load data for object !object', ['!object' => $object]));
+    }
+    $this->output()->writeln(print_r($objectDescription->data, 1));
+  }
+
+  /**
+   * Retrieve object record types.
+   *
+   * @param $object
+   *   The object name in Salesforce.
+   *
+   * @command salesforce:describe-record-types
+   * @aliases sfdrt,sf-describe-record-types
+   *
+   * @field-labels
+   *   active: Active
+   *   available: Available
+   *   defaultRecordTypeMapping: Default
+   *   developerName: Developer Name
+   *   master: Master
+   *   name: Name
+   *   recordTypeId: Id
+   *   urls: URLs
+   *
+   * @default-fields name,recordTypeId,developerName,active,available,defaultRecordTypeMapping,master
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   */
+  public function describeRecordTypes($object) {
+    $objectDescription = $this->client->objectDescribe($object);
+    if (!is_object($objectDescription)) {
+      throw new \Exception(dt('Could not load data for object !object', ['!object' => $object]));
+    }
+    $data = $objectDescription->data['recordTypeInfos'];
+    // Return if we cannot load any data.
+    $rows = [];
+    foreach ($data as $rt) {
+      $rt['urls'] = implode("\n", $rt['urls']);
+      $rows[$rt['developerName']] = $rt;
+    }
+    return new RowsOfFields($rows);
+  }
+
+  /**
+   * Retrieve object metadata.
+   *
+   * @param $object
+   *   The object name in Salesforce.
+   *
+   * @command salesforce:describe-metadata
+   * @aliases sfdom,sf-describe-metadata
+   *
+   * @field-labels
+   *   actionOverrides: ActionOverrides
+   *   activateable: Activateable
+   *   compactLayoutable: CompactLayoutable
+   *   createable: Createable
+   *   custom: Custom
+   *   customSetting: CustomSetting
+   *   deletable: Deletable
+   *   deprecatedAndHidden: DeprecatedAndHidden
+   *   feedEnabled: FeedEnabled
+   *   hasSubtypes: HasSubtypes
+   *   isSubtype: IsSubtype
+   *   keyPrefix: KeyPrefix
+   *   label: Label
+   *   labelPlural: LabelPlural
+   *   layoutable: Layoutable
+   *   listviewable: Listviewable
+   *   lookupLayoutable: LookupLayoutable
+   *   mergeable: Mergeable
+   *   mruEnabled: MruEnabled
+   *   name: Name
+   *   namedLayoutInfos: NamedLayoutInfos
+   *   networkScopeFieldName: NetworkScopeFieldName
+   *   queryable: Queryable
+   *   replicateable: Replicateable
+   *   retrieveable: Retrieveable
+   *   searchLayoutable: SearchLayoutable
+   *   searchable: Searchable
+   *   supportedScopes: SupportedScopes
+   *   triggerable: Triggerable
+   *   undeletable: Undeletable
+   *   updateable: Updateable
+   *   urls: Urls
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\PropertyList
+   */
+  public function describeMetadata($object) {
+    $objectDescription = $this->client->objectDescribe($object);
+    if (!is_object($objectDescription)) {
+      throw new \Exception(dt('Could not load data for object !object', ['!object' => $object]));
+    }
+    $data = $objectDescription->data;
+    // Return if we cannot load any data.
+    unset($data['fields'], $data['childRelationships'], $data['recordTypeInfos']);
+    foreach ($data as $k => &$v) {
+      if ($k == 'supportedScopes') {
+        array_walk($v, function(&$value, $key) {
+          $value = $value['name'] . ' (' . $value['label'] . ')';
+        });
+      }
+      if (is_array($v)) {
+        if (empty($v)) {
+          $v = '';
+        }
+        else {
+          $v = implode("\n", $v) . "\n";
+        }
+      }
+    }
+    return new PropertyList($data);
+  }
+
+  /**
+   * Retrieve all the metadata for an object, including information about each field, URLs, and child relationships.
+   *
+   * @param $object
+   *   The object name in Salesforce.
+   *
+   * @command salesforce:describe-fields
+   * @aliases salesforce:describe-object,sfdo,sfdf,sf-describe-fields
+   * @usage drush sfdo Contact
+   *   Show metadata about Contact SObject type.
+   *
+   * @field-labels
+   *   aggregatable: Aggregatable
+   *   aiPredictionField: AiPredictionField
+   *   autoNumber: AutoNumber
+   *   byteLength: ByteLength
+   *   calculated: Calculated
+   *   calculatedFormula: CalculatedFormula
+   *   cascadeDelete: CascadeDelete
+   *   caseSensitive: CaseSensitive
+   *   compoundFieldName: CompoundFieldName
+   *   controllerName: ControllerName
+   *   createable: Createable
+   *   custom: Custom
+   *   defaultValue: DefaultValue
+   *   defaultValueFormula: DefaultValueFormula
+   *   defaultedOnCreate: DefaultedOnCreate
+   *   dependentPicklist: DependentPicklist
+   *   deprecatedAndHidden: DeprecatedAndHidden
+   *   digits: Digits
+   *   displayLocationInDecimal: DisplayLocationInDecimal
+   *   encrypted: Encrypted
+   *   externalId: ExternalId
+   *   extraTypeInfo: ExtraTypeInfo
+   *   filterable: Filterable
+   *   filteredLookupInfo: FilteredLookupInfo
+   *   formulaTreatNullNumberAsZero: FormulaTreatNullNumberAsZero
+   *   groupable: Groupable
+   *   highScaleNumber: HighScaleNumber
+   *   htmlFormatted: HtmlFormatted
+   *   idLookup: IdLookup
+   *   inlineHelpText: InlineHelpText
+   *   label: Label
+   *   length: Length
+   *   mask: Mask
+   *   maskType: MaskType
+   *   name: Name
+   *   nameField: NameField
+   *   namePointing: NamePointing
+   *   nillable: Nillable
+   *   permissionable: Permissionable
+   *   picklistValues: PicklistValues
+   *   polymorphicForeignKey: PolymorphicForeignKey
+   *   precision: Precision
+   *   queryByDistance: QueryByDistance
+   *   referenceTargetField: ReferenceTargetField
+   *   referenceTo: ReferenceTo
+   *   relationshipName: RelationshipName
+   *   relationshipOrder: RelationshipOrder
+   *   restrictedDelete: RestrictedDelete
+   *   restrictedPicklist: RestrictedPicklist
+   *   scale: Scale
+   *   searchPrefilterable: SearchPrefilterable
+   *   soapType: SoapType
+   *   sortable: Sortable
+   *   type: Type
+   *   unique: Unique
+   *   updateable: Updateable
+   *   writeRequiresMasterRead: WriteRequiresMasterRead
+   *
+   * @default-fields label,name,type
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   */
+  public function describeFields($object) {
+    $objectDescription = $this->client->objectDescribe($object);
+    // Return if we cannot load any data.
+    if (!is_object($objectDescription)) {
+      throw new \Exception(dt('Could not load data for object !object', ['!object' => $object]));
+    }
+
+    foreach ($objectDescription->getFields() as $field => $data) {
+      if (!empty($data['picklistValues'])) {
+        $fix_data = [];
+        foreach ($data['picklistValues'] as $value) {
+          $fix_data[] = $value['value'] . ' (' . $value['label'] . ')';
+        }
+        $data['picklistValues'] = $fix_data;
+      }
+      foreach ($data as $k => &$v) {
+        if (is_array($v)) {
+          $v = implode("\n", $v);
+        }
+      }
+      $rows[$field] = $data;
+    }
+    return new RowsOfFields($rows);
+  }
+
+  /**
+   * Lists the resources available for the current API version.
+   *
+   * @command salesforce:list-resources
+   * @aliases sflr,sf-list-resources
+   * @field-labels
+   *   resource: Resource
+   *   url: URL
+   * @default-fields resource,url
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   */
+  public function listResources() {
+    $resources = $this->client->listResources();
+    if ($resources) {
+      foreach ($resources->resources as $resource => $url) {
+        $rows[$url] = ['resource' => $resource, 'url' => $url];
+      }
+      $this->output()->writeln("The following resources are available:");
+      return new RowsOfFields($rows);
+    }
+    throw new \Exception('Could not obtain a list of resources!');
+  }
+
+  /**
+   * @hook interact salesforce:read-object
+   */
+  public function interactReadObject(Input $input, Output $output) {
+    if (!$input->getArgument('id')) {
+      if (!$answer = $this->io()->ask('Enter the Salesforce id to fetch')) {
+        throw new UserAbortException();
+      }
+      $input->setArgument('id', $answer);
+    }
+  }
+
+  /**
+   * Retrieve all the data for an object with a specific ID.
+   *
+   * @command salesforce:read-object
+   * @aliases sfro,sf-read-object
+   */
+  public function readObject($id) {
+    $name = $this->client->getObjectTypeName(new SFID($id));
+    if ($object = $this->client->objectRead($name, $id)) {
+      $this->output()->writeln(dt("!type with id !id", [
+        '!type' => $object->type(),
+        '!id' => $object->id(),
+      ]));
+      $this->output()->writeln(print_r($object->fields(), 1));
+    }
+    return;
+  }
+
+  /**
+   * @hook interact salesforce:create-object
+   */
+  public function interactCreateObject(Input $input, Output $output) {
+    $format = $input->getOption('format');
+    if (empty($format)) {
+      $input->setOption('format', 'query');
+      $format = 'query';
+    }
+    elseif (!in_array($input->getOption('format'), ['query', 'json'])) {
+      throw new \Exception('Invalid format');
+    }
+
+    $this->interactObject($input, $output, 'Enter the object type to be created');
+
+    if (!$data = $this->io()->ask('Enter the object data to be created')) {
+      throw new UserAbortException();
+    }
+    $params = [];
+    switch ($format) {
+      case 'query':
+        parse_str($data, $params);
+        if (empty($params)) {
+          throw new \Exception(dt('Error when decoding data'));
+        }
+        break;
+
+      case 'json':
+        $params = json_decode($data, TRUE);
+        if (json_last_error()) {
+          throw new \Exception(dt('Error when decoding data: !error', ['!error' => json_last_error_msg()]));
+        }
+        break;
+
+    }
+    $this->input()->setArgument('data', $params);
+  }
+
+  /**
+   * Create an object with specified data.
+   *
+   * @param string $object
+   *   The object type name in Salesforce (e.g. Account).
+   * @param array $data
+   *   The data to use when creating the object (default is JSON format). Use '-' to read the data from STDIN.
+   * @param array $options An associative array of options whose values come from cli, aliases, config, etc.
+   * @option format
+   *   Format to parse the object. Use  "json" for JSON (default) or "query" for data formatted like a query string, e.g. 'Company=Foo&LastName=Bar'.
+   *   Defaults to query.
+   *
+   * @command salesforce:create-object
+   * @aliases sfco,sf-create-object
+   */
+  public function createObject($object, $data, array $options = ['format' => 'query']) {
+    if ($result = $this->client->objectCreate($object, $data)) {
+      $this->output->writeln(dt('Successfully created !object with id !id', ['!object' => $object, '!id' => (string)$result]));
+    }
+  }
+
+  /**
+   * @hook interact salesforce:query-object
+   */
+  public function interactQueryObject(Input $input, Output $output) {
+    return $this->interactObject($input, $output, 'Enter the object to be queried');
+  }
+  /**
+   * Query an object using SOQL with specified conditions.
+   *
+   * @param $object
+   *   The object type name in Salesforce (e.g. Account).
+    * @param array $options An associative array of options whose values come from cli, aliases, config, etc.
+   * @option format
+   *   Format to output the objects. Use "print_r" for print_r (default), "export" for var_export, and "json" for JSON.
+   * @option where
+   *   A WHERE clause to add to the SOQL query
+   * @option fields
+   *   A comma-separated list fields to select in the SOQL query. If absent, an API call is used to find all fields
+   * @option limit
+   *   Integer limit on the number of results to return for the query.
+   * @option order
+   *   Comma-separated fields by which to sort results. Make sure to enclose in quotes for any whitespace.
+   *
+   * @command salesforcef:query-object
+   * @aliases sfqo,sf-query-object
+   */
+  public function queryObject($object, array $options = ['format' => null, 'where' => null, 'fields' => null, 'limit' => null, 'order' => null]) {
+    $query = new SelectQuery($object);
+
+    if (!$options['fields']) {
+      $object = $this->client->objectDescribe($object);
+      $query->fields = array_keys($object->getFields());
+    }
+    else {
+      $query->fields = explode(',', $options['fields']);
+      // Query must include Id.
+      if (!in_array('Id', $query->fields)) {
+        $query->fields[] = 'Id';
+      }
+    }
+
+    $query->limit = $options['limit'];
+
+    if ($options['where']) {
+      $query->conditions = [[$options['where']]];
+    }
+
+    if ($options['order']) {
+      $query->order = [];
+      $orders = explode(',', $options['order']);
+      foreach ($orders as $order) {
+        list($field, $dir) = preg_split('/\s+/', $order, 2);
+        $query->order[$field] = $dir;
+      }
+    }
+
+    $result = $this->client->query($query);
+    foreach ($result->records() as $sfid => $record) {
+      $this->output()->writeln(print_r($record->fields(), 1));
+    }
+    $pretty_query = str_replace('+', ' ', (string) $query);
+    if (!$options['fields']) {
+      $fields = implode(',', $query->fields);
+      $pretty_query = str_replace($fields, ' * ', $pretty_query);
+    }
+    $this->output()->writeln(dt("Showing !size of !total records for query:\n!query", [
+      '!size' => count($result->records()),
+      '!total' => $result->size(),
+      '!query' => $pretty_query,
+    ]));
+  }
+
+  /**
+   * Execute a SOQL query.
+   *
+   * @param string $query
+   *   The query to execute.
+   *
+   * @command salesforce:execute-query
+   * @aliases sfeq,soql,sf-execute-query
+   */
+  public function executeQuery($query) {
+    $this->output()->writeln(print_r($this->client->apiCall('query?q=' . urlencode($query)), 1));
+  }
+
+}
-- 
GitLab