// 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
Last active
May 7, 2024 16:25
-
-
Save zzgael/be615eb0175903466bebefb73dad62bc to your computer and use it in GitHub Desktop.
HubSpot Repository
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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', | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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