Skip to content

Instantly share code, notes, and snippets.

@zzgael
Last active May 7, 2024 16:25
Show Gist options
  • Save zzgael/be615eb0175903466bebefb73dad62bc to your computer and use it in GitHub Desktop.
Save zzgael/be615eb0175903466bebefb73dad62bc to your computer and use it in GitHub Desktop.
HubSpot Repository

Usage

 // Dependencies : a PSR-compatible logger, illuminate/collections and HubSpot SDK

 // composer require illuminate/collections
 // composer require "hubspot/hubspot-php"
 // composer require Any compatible psr-compatible logger

 $repo = new HubspotRepository('xxxxx-xxxxx-xxxx') // Access token;

 // Upsert two company based on ID
 // It can insert, update and delete based on parameters
 $repo->sync('company', 'hs_object_id', [
   ["properties" => [
     'hs_object_id' => 1
     'name' => 'Entreprise 1', 
     'siret' => '3213123123'
   ],
   ["properties" => [
     'hs_object_id' => 2
     'name' => 'Entreprise 2', 
     'siret' => '3213123123'
   ],
 ])
 // For Deletion, we would need to use a fourth parameter indicating which key contains the "is deleted" flag
<?php namespace App;
enum AssociationEnum: int
{
case CONTACT_TO_COMPANY = 1;
case DEAL_TO_CONTACT = 3;
case DEAL_TO_COMPANY = 5;
case QUOTE_TO_DEAL = 64;
case QUOTE_TO_QUOTE_TEMPLATE = 286;
case QUOTE_TO_LINE_ITEM = 67;
case QUOTE_TO_TASK = 217;
case QUOTE_TO_CONTACT_SIGNER = 702;
case QUOTE_TO_COMPANY = 71;
case QUOTE_TO_CONTACT = 69;
case TONOBJECTCUSTOM_TO_DEAL = XXX;
public function category(): string
{
return match ($this) {
self::CONTACT_TO_COMPANY,
self::DEAL_TO_CONTACT,
self::DEAL_TO_COMPANY,
self::QUOTE_TO_DEAL,
self::QUOTE_TO_QUOTE_TEMPLATE,
self::QUOTE_TO_LINE_ITEM,
self::QUOTE_TO_COMPANY,
self::QUOTE_TO_TASK,
self::QUOTE_TO_CONTACT_SIGNER,
self::QUOTE_TO_CONTACT => 'HUBSPOT_DEFINED',
self::TONOBJECTCUSTOM_TO_DEAL => 'USER_DEFINED',
};
}
}
<?php namespace App;
use App\AssociationEnum;
// composer require illuminate/collections
use Illuminate\Support\Collection;
// Any PSR-compatible logger
use Psr\Log\LoggerInterface;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicAssociation;
use HubSpot\Client\Crm\Associations\Model\PublicAssociation;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Associations\V4\Model\BatchInputPublicFetchAssociationsBatchRequest;
use HubSpot\Client\Crm\Associations\V4\Model\BatchResponsePublicAssociationMultiWithLabel;
use HubSpot\Client\Crm\Associations\V4\Model\BatchResponsePublicAssociationMultiWithLabelWithErrors;
use HubSpot\Client\Crm\Associations\V4\Model\PublicFetchAssociationsBatchRequest;
use HubSpot\Client\Crm\Companies\ApiException;
use HubSpot\Client\Crm\Objects\Model\BatchInputSimplePublicObjectBatchInput;
use HubSpot\Client\Crm\Objects\Model\BatchInputSimplePublicObjectId;
use HubSpot\Client\Crm\Objects\Model\BatchInputSimplePublicObjectInputForCreate;
use HubSpot\Client\Crm\Objects\Model\BatchReadInputSimplePublicObjectId;
use HubSpot\Client\Crm\Objects\Model\BatchResponseSimplePublicObject;
use HubSpot\Client\Crm\Objects\Model\BatchResponseSimplePublicObjectWithErrors;
use HubSpot\Client\Crm\Objects\Model\Error;
use HubSpot\Client\Crm\Objects\Model\Filter;
use HubSpot\Client\Crm\Objects\Model\FilterGroup;
use HubSpot\Client\Crm\Objects\Model\PublicObjectSearchRequest;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObject;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectId;
use HubSpot\Client\Crm\Objects\Model\StandardError;
use HubSpot\Delay;
use HubSpot\Discovery\Discovery;
use HubSpot\Factory;
use HubSpot\RetryMiddlewareFactory;
class HubspotRepository
{
private Discovery $client;
public function __construct(
// Substitue or inject any PSR-compatible logger
private readonly LoggerInterface $logger,
private readonly string $accessToken,
)
{
$handlerStack = HandlerStack::create();
$handlerStack->push(
RetryMiddlewareFactory::createRateLimitMiddleware(
Delay::getConstantDelayFunction()
)
);
$handlerStack->push(
RetryMiddlewareFactory::createInternalErrorsMiddleware(
Delay::getExponentialDelayFunction(2)
)
);
$client = new Client(['handler' => $handlerStack]);
$this->client = Factory::createWithAccessToken($this->accessToken, $client);
}
/**
* @throws \HubSpot\Client\Crm\Objects\ApiException
*/
public function upsertCompany(array $properties): string
{
return $this->sync('company', 'hs_object_id', [["properties" => $properties]])[0];
}
/**
* @throws \HubSpot\Client\Crm\Objects\ApiException
*/
public function upsertContact(array $properties, ?string $companyId = null): string
{
return $this->sync('contact', 'email', [[
"properties" => $properties,
"associations" => $companyId ? [
[AssociationEnum::CONTACT_TO_COMPANY, $companyId]
] : null
]])[0];
}
/**
* @throws \HubSpot\Client\Crm\Objects\ApiException
*/
public function upsertDeal(array $properties, ?string $contactId = null, ?string $companyId = null)
{
//dd($this->client->crm()->associations()->schema()->typesApi()->getAll('quote', 'deal'));
return $this->sync('deal', 'hs_object_id', [[
"properties" => $properties,
"associations" => array_filter([
$contactId ? [AssociationEnum::DEAL_TO_CONTACT, $contactId] : null,
$companyId ? [AssociationEnum::DEAL_TO_COMPANY, $companyId] : null,
])
]])[0];
}
/**
* @throws \HubSpot\Client\Crm\Objects\ApiException
*/
public function upsertLineItems(array $lineItems): array
{
return $this->sync('line_item', 'hs_object_id', collect($lineItems)->map(function ($lineItem) {
return [
"properties" => $lineItem
];
}));
}
/**
* @throws ApiException
*/
public function getCompanies(array $filters = []): array
{
$searchRequest = new \HubSpot\Client\Crm\Companies\Model\PublicObjectSearchRequest([
'filter_groups' => [
[
'filters' => $filters,
],
],
'properties' => [
'name',
'siret',
'address',
'address2',
'zip',
'city',
'phone',
],
'limit' => 100,
]);
$response = $this->client->crm()->companies()->searchApi()->doSearch($searchRequest);
return $response;
}
/**
* @throws \HubSpot\Client\Crm\Contacts\ApiException
*/
public function getContacts(array $filterGroups = []): array
{
$searchRequest = new \HubSpot\Client\Crm\Contacts\Model\PublicObjectSearchRequest([
'filter_groups' => array_map(fn($filterGroup) => ['filters' => $filterGroup], $filterGroups),
'properties' => array_keys(get_object_vars(new ContactDto)),
]);
//dd($searchRequest->getFilterGroups());
$response = $this->client->crm()->contacts()->searchApi()->doSearch($searchRequest);
return $response;
}
/**
* @throws \HubSpot\Client\Crm\Deals\ApiException
* @throws \HubSpot\Client\Crm\Pipelines\ApiException
* @throws \HubSpot\Client\Crm\Objects\ApiException
*/
public function getDeals(array $filters = []): array
{
$searchRequest = new \HubSpot\Client\Crm\Deals\Model\PublicObjectSearchRequest([
'filter_groups' => [['filters' => $filters]],
'properties' => [
'dealname',
'dealstage',
'amount',
'closedate',
'hubspot_owner_id',
'hs_object_id',
'pipeline'
],
]);
$response = $this->client->crm()->deals()->searchApi()->doSearch($searchRequest);
//$pipelinesResponse = $this->client->crm()->pipelines()->pipelinesApi()->getAll('deals');
return $response;
}
/**
* @throws \HubSpot\Client\Crm\Deals\ApiException
* @throws \HubSpot\Client\Crm\Pipelines\ApiException
*/
public function getDeal(string $dealId): DealDto
{
$deals = $this->getDeals([[
'propertyName' => 'hs_object_id',
'operator' => 'EQ',
'value' => $dealId,
]]);
return $deals[0];
}
/**
* @throws \HubSpot\Client\Crm\Owners\ApiException
*/
public function getOwnersByRole(string $role): array
{
$owners = $this->client->crm()->owners()->ownersApi()->getPage();
$filteredOwners = array_filter($owners->getResults(), function ($owner) use ($role) {
$teamIds = array_map(fn($team) => $team->getId(), $owner->getTeams() ?? []);
return in_array($role, $teamIds);
});
return array_values(array_map(fn($owner) => [
'id' => $owner->getId(),
'name' => $owner->getFirstName() . ' ' . $owner->getLastName()
], $filteredOwners));
}
/**
* @throws \HubSpot\Client\Crm\Objects\ApiException
*/
public function sync(string $objectName, string $primaryKey, array $inputObjects, ?string $isDeletedKey = null): array
{
$batchApi = $this->client
->crm()
->objects()
->batchApi();
$objectsToCreate = collect();
$objectsToUpdate = collect();
$objectsToDelete = collect();
$chunkSize = 100;
$existingObjectsResults = [];
$inputObjectsChunks = collect($inputObjects)->chunk($chunkSize);
foreach ($inputObjectsChunks as $i => $inputObjectsChunk) {
$this->logger->debug("Reading chunk $i of existing $objectName from Hubspot ");
$responseExistingObjects = $batchApi->read($objectName, new BatchReadInputSimplePublicObjectId([
"id_property" => $primaryKey,
"inputs" => $inputObjectsChunk
->map(fn($o) => ["id" => $o["properties"][$primaryKey]])
->values()
->filter(fn($o) => $o["id"])
->toArray(),
"properties" => [$primaryKey]
]));
$existingObjectsResults = array_merge($existingObjectsResults, $responseExistingObjects->getResults());
}
/** @var SimplePublicObject[] $existingObjectsByKey */
$existingObjectsByKey = collect($existingObjectsResults)->keyBy(
fn(SimplePublicObject $object) => $object->getProperties()[$primaryKey]
)->toArray();
foreach ($inputObjects as $inputObject) {
$objectId = $inputObject["properties"][$primaryKey];
$existingObject = $existingObjectsByKey[$objectId] ?? null;
$objectDefinition = [
"properties" => collect($inputObject["properties"])->except('hs_object_id')->toArray(),
"associations" => collect($inputObject["associations"] ?? null)
->map(function (array $association) {
if (($association[0] ?? null) instanceof AssociationEnum) {
return [
"to" => ["id" => $association[1]],
"types" => [[
"associationCategory" => $association[0]->category(),
"associationTypeId" => $association[0]->value
]]
];
}
return $association;
})
->values()
->toArray()
];
if ($isDeletedKey) {
$objectIsDeletedKey = $inputObject["properties"][$isDeletedKey];
if ($objectIsDeletedKey) {
if ($existingObject) {
$objectsToDelete[] = [
"id" => $existingObject->getId(),
...$objectDefinition
];
}
continue;
}
}
if ($existingObject) {
$objectsToUpdate[] = [
"id" => $existingObject->getId(),
...$objectDefinition
];
} else {
$objectsToCreate[] = [
...$objectDefinition
];
}
}
$objectIds = [];
if (count($objectsToCreate)) {
$this->logger->debug("Creating " . count($objectsToCreate) . " $objectName in Hubspot");
$objectsToCreate->chunk($chunkSize)->each(function (Collection $objectsChunk) use (&$objectIds, $objectName, $batchApi) {
$objectIds = [
...$objectIds,
...collect($batchApi->create($objectName, new BatchInputSimplePublicObjectInputForCreate([
"inputs" => $objectsChunk->values()->toArray()
]))->getResults())
->map(fn($object) => $object['id'])
->toArray()
];
});
}
if (count($objectsToUpdate)) {
$objectIds = [
...$objectIds,
...$objectsToUpdate->pluck('id')->toArray()
];
$this->logger->debug("Updating " . count($objectsToUpdate) . " $objectName in Hubspot");
$objectsToUpdate->chunk($chunkSize)->each(fn(Collection $objectsChunk) => $batchApi
->update($objectName, new BatchInputSimplePublicObjectBatchInput([
"inputs" => $objectsChunk->values()->toArray()
]))
);
}
if (count($objectsToDelete)) {
$this->logger->debug("Deleting " . count($objectsToDelete) . " $objectName in Hubspot");
$objectsToDelete->pluck("id")->chunk($chunkSize)->each(fn(Collection $objectsChunk) => $batchApi
->archive($objectName, new BatchInputSimplePublicObjectId([
"inputs" => $objectsChunk->values()->toArray()
]))
);
}
return $objectIds;
}
/**
* @throws \HubSpot\Client\Crm\Associations\ApiException
*/
public function batchAssociate(array $fromIds, string $toId, string $fromObjectType, string $toObjectType, string $associationType): void
{
$this->logger->debug("Associating " . count($fromIds) . " $fromObjectType to $toObjectType");
collect($fromIds)->chunk(100)->each(function (Collection $fromIdsChunk) use ($toObjectType, $fromObjectType, $associationType, $toId) {
$batchInput = new BatchInputPublicAssociation([
'inputs' => $fromIdsChunk
->map(fn($fromId) => new PublicAssociation([
'from' => new PublicObjectId(['id' => $fromId]),
'to' => new PublicObjectId(['id' => $toId]),
'type' => $associationType
]))
->values()
->toArray()
]);
$this->client->crm()->associations()->batchApi()->create(
$fromObjectType,
$toObjectType,
$batchInput
);
});
}
/**
* @param string $objectType
* @param string $propertyName
* @param $value
* @return Collection
* @throws \HubSpot\Client\Crm\Objects\ApiException
* @throws \Exception
*/
public function search(string $objectType, string $propertyName, $value, array $properties): Collection
{
$filter = new Filter([
'property_name' => $propertyName,
'operator' => 'EQ',
'value' => $value,
]);
$filterGroup = new FilterGroup([
'filters' => [$filter]
]);
$publicObjectSearchRequest = new PublicObjectSearchRequest([
'filter_groups' => [$filterGroup],
'properties' => $properties,
]);
$results = $this->client->crm()->objects()->searchApi()->doSearch($objectType, $publicObjectSearchRequest)->getResults();
if (!count($results)) {
throw new \Exception("No $objectType found with $propertyName = $value");
}
return $results;
}
/**
* @throws \HubSpot\Client\Crm\Associations\V4\ApiException
*/
private function getAssociations(string $from, string $to, string $toObjectId): BatchResponsePublicAssociationMultiWithLabel|BatchResponsePublicAssociationMultiWithLabelWithErrors
{
return $this->client->crm()->associations()->v4()->batchApi()->getPage($from, $to,
new BatchInputPublicFetchAssociationsBatchRequest([
'inputs' => [new PublicFetchAssociationsBatchRequest([
'id' => $toObjectId,
])],
])
);
}
/**
* @throws \HubSpot\Client\Crm\Objects\ApiException
* @throws \Exception
*/
private function read(string $objectName, string $objectProperty, array|Collection $objectPropertyValues, array $properties): BatchResponseSimplePublicObject|BatchResponseSimplePublicObjectWithErrors|Error
{
if(!count($objectPropertyValues)) {
return new BatchResponseSimplePublicObject(["results" => []]);
}
if($objectPropertyValues instanceof Collection) {
$objectPropertyValues = $objectPropertyValues->map(fn($objectPropertyValue) => new SimplePublicObjectId([
"id" => (string)$objectPropertyValue
]))->toArray();
}
$response = $this->client->crm()->objects()->batchApi()->read($objectName, new BatchReadInputSimplePublicObjectId([
'id_property' => $objectProperty,
'inputs' => $objectPropertyValues,
'properties' => $properties,
]));
if($response instanceof BatchResponseSimplePublicObjectWithErrors) {
$errors = collect($response->getErrors())
->map(fn(StandardError $error) => collect($error)->filter());
throw new \Exception($errors);
}
return $response;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment