Skip to content

Instantly share code, notes, and snippets.

@Korbeil
Last active October 15, 2024 08:43
Show Gist options
  • Save Korbeil/7451afbff73da572f0500375d6f74805 to your computer and use it in GitHub Desktop.
Save Korbeil/7451afbff73da572f0500375d6f74805 to your computer and use it in GitHub Desktop.
<?php
class PimListener
{
public const CACHE_DIRECTORY = '%s/Proxies/';
public const PROXY_NAMESPACE = 'PimProxy';
private HttpClientInterface $pimHttpClient;
private LazyLoadingGhostFactory $factory;
private array $proxiesBuffer = [];
private array $arrayBuffer = [];
private array $arrayResults = [];
public function __construct(
private SerializerInterface $serializer,
private string $cacheDir,
private string $pimApiUrl,
) {
$this->buildHttpClient();
$this->factory = new LazyLoadingGhostFactory($this->makeConfiguration($cacheDir));
}
private function buildHttpClient(): void
{
if (isset($this->pimHttpClient)) {
unset($this->pimHttpClient); // destroy it cleanly
}
$options = new HttpOptions();
$options->setBaseUri($this->pimApiUrl);
$options->setHeaders([
'User-Agent' => 'PimListener', // a clean User-Agent so you can track HTTP calls
'Accept' => 'application/json',
]);
$options->setTimeout(120);
// this is something that came later on, to make this service resettable I make
// the CurlHttpClient inside the PimListener, for some very heavy exports this is required.
$this->pimHttpClient = new CurlHttpClient($options->toArray());
}
private function makeRequest(Endpoint $endpoint): ResponseInterface
{
$queryString = $endpoint->getQueryString();
$uriGlue = false === \mb_strpos($endpoint->getUri(), '?') ? '?' : '&';
$uri = $queryString !== '' ? $endpoint->getUri() . $uriGlue . $queryString : $endpoint->getUri();
try {
return $this->pimHttpClient->request($endpoint->getMethod(), $uri);
} catch (ClientException $exception) {
throw new PimException('Pim Client Exception', 0, $exception);
}
}
public function postLoad(PostLoadEventArgs $args): void
{
/** @var object|PimInterface $entity */
$entity = $args->getObject();
if (!$entity instanceof PimInterface) {
return;
}
/** @var string $uuid */
$uuid = $entity->getPimUuid();
$entity->setPimObject($this->fetchProxy($entity, $uuid));
}
/**
* @param PimInterface[] $entities
*/
public function preload(array $entities): void
{
foreach ($entities as $entity) {
[$modelClass, $endpointClass] = $entity->getPimModelConfiguration();
if (!\array_key_exists($modelClass, $this->arrayBuffer)) {
$this->arrayBuffer[$modelClass] = [];
$this->arrayResults[$modelClass] = [];
}
if (\array_key_exists($entity->getPimUuid(), $this->arrayResults[$modelClass])) {
continue; // Do not fetch data we already have
}
$endpoint = new $endpointClass($entity->getPimUuid());
$response = $this->makeRequest($endpoint);
$this->arrayResults[$modelClass][$entity->getPimUuid()] = new Result($response, $modelClass, $this->serializer);
}
}
private function fetchProxy(PimInterface $entity, string $uuid): object
{
[$modelClass, ] = $entity->getPimModelConfiguration();
if (!\array_key_exists($modelClass, $this->proxiesBuffer)) {
$this->proxiesBuffer[$modelClass] = [];
}
if (!\array_key_exists($uuid, $this->proxiesBuffer[$modelClass])) {
$this->proxiesBuffer[$modelClass][$uuid] = $this->makeProxy($entity, $uuid);
}
return $this->proxiesBuffer[$modelClass][$uuid];
}
/**
* Ghost Proxy object factory, the idea is to generate a proxy object that will lazy-load PIM data when needed.
*
* We have an initializer method that is fired when we try to access any object property and ProxyManager factory
* is used to generate needed proxy.
* The proxy class is cached (in order to avoid repeated Reflection calls), see `makeConfiguration` for more details
*/
private function makeProxy(PimInterface $entity, string $uuid): object
{
$pimModelConfiguration = $entity->getPimModelConfiguration();
$initializer = function (
GhostObjectInterface $ghostObject,
string $method,
array $parameters,
&$initializer,
array $properties,
) use ($pimModelConfiguration, $uuid) {
$initializer = null; // disable initialization
// We only want to initialize objects that fit our entity modelClass
[$modelClass, ] = $entity->getPimModelConfiguration();
if (!$ghostObject instanceof $modelClass) {
return false;
}
$model = $this->loadModel($uuid, $entity);
// Checking if we got a result from PIM API
if (!$model instanceof $modelClass) {
return false;
}
/*
* For all object properties, we take property name then we guess the mutator method
* and we use it for each property in order to hydrate the object
*/
foreach (\array_keys($properties) as $property) {
$parts = explode("\0", $property);
/** @var string $variable */
$variable = \array_pop($parts);
$mutator = sprintf('get%s', \ucfirst($variable));
$callable = [$model, $mutator];
if (\is_callable($callable)) {
$properties[$property] = \call_user_func($callable);
}
}
return true;
};
[$modelClass, ] = $entity->getPimModelConfiguration();
$ghostedPimModel = $this->factory->createProxy($modelClass, $initializer);
return $ghostedPimModel;
}
protected function loadModel(string $uuid, object $entity): object
{
[$modelClass, $endpointClass] = $entity->getPimModelConfiguration();
if (!\array_key_exists($modelClass, $this->arrayBuffer)) {
$this->arrayBuffer[$modelClass] = [];
$this->arrayResults[$modelClass] = [];
}
if (!\array_key_exists($uuid, $this->arrayResults[$modelClass])) {
$endpoint = new $endpointClass($uuid);
$response = $this->makeRequest($endpoint);
$this->arrayResults[$modelClass][$uuid] = new Result($response, $modelClass, $this->serializer);
}
if (!\array_key_exists($uuid, $this->arrayBuffer[$modelClass])) {
/** @var Result $result */
$result = $this->arrayResults[$modelClass][$uuid];
try {
$this->arrayBuffer[$modelClass][$uuid] = $result->toObject();
} catch (ClientException $exception) {
throw new PimException(sprintf('Pim Client Exception (%s: #%s)', $modelClass, $uuid), 0, $exception);
}
}
return $this->arrayBuffer[$modelClass][$uuid];
}
private function makeConfiguration(string $cacheDir): Configuration
{
$directory = sprintf(self::CACHE_DIRECTORY, $cacheDir);
if (!\is_dir($directory)) {
\mkdir($directory);
}
$configuration = new Configuration();
$configuration->setProxiesTargetDir($directory);
$configuration->setProxiesNamespace(self::PROXY_NAMESPACE);
$configuration->setGeneratorStrategy(new FileWriterGeneratorStrategy(
new FileLocator($directory),
));
return $configuration;
}
public function clear(bool $clearHttpClient = false, bool $clearOnlyProductCache = false): void
{
if ($clearOnlyProductCache) {
unset($this->arrayResults[ProductDTO::class]);
unset($this->arrayBuffer[ProductDTO::class]);
unset($this->proxiesBuffer[ProductDTO::class]);
} else {
$this->arrayResults = $this->arrayBuffer = $this->proxiesBuffer = [];
}
if ($clearHttpClient) {
$this->buildHttpClient();
}
}
}
<?php
class PimResult
{
private bool $initialized;
public function __construct(
private ResponseInterface $response,
private string $modelClass,
private SerializerInterface $serializer,
) {
$this->initialized = false;
}
public function isInitialized(): bool
{
return $this->initialized;
}
public function getStatusCode(): int
{
$this->initialized = true;
return $this->response->getStatusCode();
}
public function toObject(): ?object
{
/** @var null|object $deserialized */
$deserialized = $this->serializer->deserialize($this->getContents(), $this->modelClass, 'json');
return $deserialized;
}
public function getContents(): string
{
$this->initialized = true;
return $this->response->getContent();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment