dynamic_image_generator-1.0.x-dev/src/Service/DynamicImageGeneratorService.php

src/Service/DynamicImageGeneratorService.php
<?php

namespace Drupal\dynamic_image_generator\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Template\TwigEnvironment;
use Drupal\Core\Utility\Token;
use Drupal\file\FileInterface;
use Drupal\file\Entity\File;
use Drupal\media\Entity\Media;
use GuzzleHttp\ClientInterface;
use Twig\Error\Error as TwigError;

/**
 * Service for generating dynamic images through the third-party API.
 */
class DynamicImageGeneratorService {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The logger service.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * The config factory service.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The Twig environment service.
   *
   * @var \Drupal\Core\Template\TwigEnvironment
   */
  protected $twig;

  /**
   * The renderer service.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The token service.
   *
   * @var \Drupal\Core\Utility\Token
   */
  protected $token;

  /**
   * The HTTP client.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

  /**
   * Constructs a DynamicImageGeneratorService object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system service.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory service.
   * @param \Drupal\Core\Template\TwigEnvironment $twig
   *   The Twig environment service.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
   * @param \Drupal\Core\Utility\Token $token
   *   The token service.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The HTTP client.
   */
  public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    FileSystemInterface $file_system,
    LoggerChannelFactoryInterface $logger_factory,
    ConfigFactoryInterface $config_factory,
    TwigEnvironment $twig,
    RendererInterface $renderer,
    Token $token,
    ClientInterface $http_client
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->fileSystem = $file_system;
    $this->logger = $logger_factory->get('dynamic_image_generator');
    $this->configFactory = $config_factory;
    $this->twig = $twig;
    $this->renderer = $renderer;
    $this->token = $token;
    $this->httpClient = $http_client;
  }

  /**
   * Generate preview image without saving (for real-time preview).
   */
  public function generatePreviewImage($poster_entity_id, array $custom_tokens = []) {
    try {
      /** @var \Drupal\poster_generator\Entity\PosterEntity $poster_entity */
      $poster_entity = $this->entityTypeManager->getStorage('poster_entity')->load($poster_entity_id);
      
      if (!$poster_entity) {
        $this->logger->error('Poster entity with ID @id not found for preview.', ['@id' => $poster_entity_id]);
        return NULL;
      }
      
      $this->logger->info('Generating preview image for poster @poster_id (NO SAVE)', [
        '@poster_id' => $poster_entity_id,
      ]);
      
      $html_template = $poster_entity->getHTML();
      $css_template = $poster_entity->getCSS();
      
      // Prepare image tokens
      $image_tokens = $this->prepareImageTokens($poster_entity);
      
      // Process tokens with real-time form data
      $all_tokens = array_merge($image_tokens, $custom_tokens);
      $all_tokens['site_name'] = \Drupal::config('system.site')->get('name');
      $all_tokens['date'] = \Drupal::service('date.formatter')->format(time(), 'medium');
      
      // Process Drupal tokens first
      if (!empty($custom_tokens)) {
        $html_template = $this->processTokensInTemplate($html_template, $custom_tokens);
        $css_template = $this->processTokensInTemplate($css_template, $custom_tokens);
      }
      
      // Replace image tokens in templates
      $html_template = $this->replaceImageTokens($html_template, $image_tokens);
      $css_template = $this->replaceImageTokens($css_template, $image_tokens);
      
      // Render templates with Twig context
      $html = $this->renderTwigTemplate($html_template, $all_tokens);
      $css = $this->renderTwigTemplate($css_template, $all_tokens);
      
      // Sanitize content
      $html = $this->sanitizeContent($html);
      $css = $this->sanitizeContent($css);
      
      // Call API to generate preview (returns temporary URL)
      $api_image_url = $this->callPosterGenerationApi($html, $css);
      
      if ($api_image_url) {
        $this->logger->info('Preview image generated successfully: @url', ['@url' => $api_image_url]);
        return $api_image_url; // Return direct API URL for preview (no saving)
      } else {
        $this->logger->error('Failed to generate preview image via API');
      }
      
      return NULL;
    }
    catch (\Exception $e) {
      $this->logger->error('Error generating preview image: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Generate poster image and save to dynamic image media first, then reuse file.
   */
  public function generatePosterImage($poster_entity_id, array $custom_tokens = [], $entity_id = NULL) {
    try {
      /** @var \Drupal\poster_generator\Entity\PosterEntity $poster_entity */
      $poster_entity = $this->entityTypeManager->getStorage('poster_entity')->load($poster_entity_id);
      
      if (!$poster_entity) {
        $this->logger->error('Poster entity with ID @id not found.', ['@id' => $poster_entity_id]);
        return NULL;
      }
      
      $this->logger->info('Starting poster generation for poster @poster_id, entity @entity_id', [
        '@poster_id' => $poster_entity_id,
        '@entity_id' => $entity_id ?: 'NULL',
      ]);
      
      $content_type = $poster_entity->getContentType();
      $html_template = $poster_entity->getHTML();
      $css_template = $poster_entity->getCSS();
      
      $entity = NULL;
      if ($entity_id) {
        $entity_type = 'node';
        
        try {
          $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id);
        }
        catch (\Exception $e) {
          $this->logger->warning('Failed to load entity with ID @id: @error', [
            '@id' => $entity_id,
            '@error' => $e->getMessage(),
          ]);
        }
        
        if ($entity && $entity->getEntityTypeId() === 'node') {
          if ($entity->bundle() !== $content_type) {
            $this->logger->warning('Entity with ID @id is of type @actual_type but @expected_type was expected.', [
              '@id' => $entity_id,
              '@actual_type' => $entity->bundle(),
              '@expected_type' => $content_type,
            ]);
            $entity = NULL;
          }
        }
      }
      
      if ($entity) {
        $entity_type = $entity->getEntityTypeId();
        $token_data = [$entity_type => $entity];
        $html_template = $this->processTokens($html_template, $token_data);
        $css_template = $this->processTokens($css_template, $token_data);
      }
      
      $image_tokens = $this->prepareImageTokens($poster_entity);
      $context = array_merge($custom_tokens, $image_tokens);
      $context['site_name'] = \Drupal::config('system.site')->get('name');
      $context['date'] = \Drupal::service('date.formatter')->format(time(), 'medium');
      
      $html_template = $this->replaceImageTokens($html_template, $image_tokens);
      $css_template = $this->replaceImageTokens($css_template, $image_tokens);
      
      $html = $this->renderTwigTemplate($html_template, $context);
      $css = $this->renderTwigTemplate($css_template, $context);
      
      $html = $this->sanitizeContent($html);
      $css = $this->sanitizeContent($css);
      
      $api_image_url = $this->callPosterGenerationApi($html, $css);
      
      if ($api_image_url) {
        $this->logger->info('API generated image at: @api_url', ['@api_url' => $api_image_url]);
        
        // STEP 1: Create dynamic image media first
        $dynamic_media_entity = $this->createDynamicImageMedia($poster_entity_id, $entity_id, $api_image_url);
        
        if ($dynamic_media_entity) {
          $this->logger->info('Created dynamic image media @media_id', [
            '@media_id' => $dynamic_media_entity->id(),
          ]);
          
          // STEP 2: Reuse the file for target field if entity and target field exist
          if ($entity && $poster_entity->getTargetField()) {
            $result = $this->reuseFileForTargetField($entity, $poster_entity->getTargetField(), $dynamic_media_entity);
            $this->logger->notice('Reused file for target field @field: @result', [
              '@field' => $poster_entity->getTargetField(),
              '@result' => $result ? 'SUCCESS' : 'FAILED',
            ]);
          }
          
          $public_url = $this->getImageUrlFromMedia($dynamic_media_entity);
          if ($public_url) {
            $this->logger->info('Successfully generated and saved dynamic image. Public URL: @url', ['@url' => $public_url]);
            return $public_url;
          } else {
            $this->logger->error('Failed to get public URL from dynamic media entity @media_id', [
              '@media_id' => $dynamic_media_entity->id(),
            ]);
          }
        } else {
          $this->logger->error('Failed to create dynamic image media entity');
        }
      } else {
        $this->logger->error('API failed to generate poster image');
      }
      
      return NULL;
    }
    catch (\Exception $e) {
      $this->logger->error('Error generating poster: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Process tokens in template with real data for previews.
   */
  protected function processTokensInTemplate($template, array $custom_tokens) {
    if (empty($template) || empty($custom_tokens)) {
      return $template;
    }
    
    // Replace Drupal tokens manually for preview
    foreach ($custom_tokens as $key => $value) {
      if (strpos($key, 'node:') === 0) {
        // Direct node token replacement
        $template = str_replace('[' . $key . ']', $value, $template);
      } else {
        // Convert field tokens to node format
        $template = str_replace('[node:title]', $custom_tokens['title'] ?? '', $template);
        $template = str_replace('[node:body]', $custom_tokens['body'] ?? '', $template);
        
        // Convert custom field tokens
        if (strpos($key, 'field_') !== 0 && $key !== 'title' && $key !== 'body') {
          $template = str_replace('[node:field_' . $key . ']', $value, $template);
        }
      }
    }
    
    return $template;
  }

  /**
   * Create dynamic image media entity (always save generated images here first).
   */
  protected function createDynamicImageMedia($poster_entity_id, $entity_id, $api_image_url) {
    try {
      $this->logger->info('Creating dynamic image media for poster @poster_id, entity @entity_id', [
        '@poster_id' => $poster_entity_id,
        '@entity_id' => $entity_id,
      ]);
      
      $file = $this->downloadAndSaveImageFile($api_image_url, $entity_id);
      
      if (!$file) {
        $this->logger->error('Failed to create file for dynamic image');
        return NULL;
      }
      
      $this->logger->info('Created file @file_id for dynamic image', [
        '@file_id' => $file->id(),
      ]);

      // Check if we have the dynamic_image media type
      $media_type_storage = $this->entityTypeManager->getStorage('media_type');
      $available_types = $media_type_storage->loadMultiple();
      
      $media_bundle = 'dynamic_image';
      $image_field_name = 'field_dynamic_image';
      
      // Check if we have the dynamic_image media type
      if (isset($available_types['dynamic_image'])) {
        $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
        foreach ($bundle_fields as $field_name => $field_definition) {
          if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
            $image_field_name = $field_name;
            break;
          }
        }
        $this->logger->info('Using dynamic_image media type with field @field for dynamic image', ['@field' => $image_field_name]);
      } else {
        // Fallback to image media type if dynamic_image doesn't exist
        if (isset($available_types['image'])) {
          $media_bundle = 'image';
          $image_field_name = 'field_media_image';
          $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'image');
          foreach ($bundle_fields as $field_name => $field_definition) {
            if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
              $image_field_name = $field_name;
              break;
            }
          }
          $this->logger->info('dynamic_image media type not found, using image media type with field @field', ['@field' => $image_field_name]);
        } else {
          // Use first available media type as last resort
          $first_type = reset($available_types);
          if ($first_type) {
            $media_bundle = $first_type->id();
            $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', $media_bundle);
            foreach ($bundle_fields as $field_name => $field_definition) {
              if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
                $image_field_name = $field_name;
                break;
              }
            }
          }
          $this->logger->warning('No dynamic_image or image media types found, using @bundle with field @field', [
            '@bundle' => $media_bundle,
            '@field' => $image_field_name,
          ]);
        }
      }

      // Create dynamic image media entity with proper metadata
      $media_data = [
        'bundle' => $media_bundle,
        'name' => 'Dynamic Image - ' . ($entity_id ? "Entity $entity_id" : 'Preview') . ' - ' . date('Y-m-d H:i:s'),
        $image_field_name => [
          'target_id' => $file->id(),
          'alt' => 'Generated dynamic image - ' . date('Y-m-d H:i:s'),
        ],
        'uid' => \Drupal::currentUser()->id(),
      ];
      
      // Add custom fields if they exist for dynamic_image media type
      if ($media_bundle === 'dynamic_image') {
        $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
        
        // Add template reference if field exists
        if (isset($bundle_fields['field_template_id'])) {
          $media_data['field_template_id'] = $poster_entity_id;
        }
        
        // Add source entity reference if field exists  
        if (isset($bundle_fields['field_source_entity']) && $entity_id) {
          $media_data['field_source_entity'] = $entity_id;
        }
      }
      
      $media = Media::create($media_data);
      $media->save();
      
      $this->logger->notice('Created dynamic image media entity @media_id (bundle: @bundle) for poster @poster_id and entity @entity_id with file @file_id', [
        '@media_id' => $media->id(),
        '@bundle' => $media_bundle,
        '@poster_id' => $poster_entity_id,
        '@entity_id' => $entity_id,
        '@file_id' => $file->id(),
      ]);
      
      return $media;
    }
    catch (\Exception $e) {
      $this->logger->error('Error creating dynamic image media: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Reuse file from dynamic image media for target field.
   */
  protected function reuseFileForTargetField(EntityInterface $entity, $field_name, Media $source_media) {
    try {
      if (!$entity->hasField($field_name)) {
        $this->logger->error('Entity @type @id does not have field @field.', [
          '@type' => $entity->getEntityTypeId(),
          '@id' => $entity->id(),
          '@field' => $field_name,
        ]);
        return FALSE;
      }
      
      $field_definition = $entity->getFieldDefinition($field_name);
      $field_type = $field_definition->getType();
      
      $this->logger->info('Reusing file from dynamic media @media_id for @field_type field @field on entity @entity_id', [
        '@media_id' => $source_media->id(),
        '@field_type' => $field_type,
        '@field' => $field_name,
        '@entity_id' => $entity->id(),
      ]);
      
      if ($field_type === 'image') {
        // For image fields, reuse the file directly
        $source_image_field = $this->getImageFieldFromMedia($source_media);
        
        if ($source_image_field) {
          $file_id = $source_image_field->target_id;
          $alt_text = $source_image_field->alt ?: 'Generated dynamic image';
          
          $entity->set($field_name, [
            'target_id' => $file_id,
            'alt' => $alt_text,
          ]);
          
          $this->logger->info('Reused file @file_id for image field @field', [
            '@file_id' => $file_id,
            '@field' => $field_name,
          ]);
        } else {
          $this->logger->error('No valid image field found in source media @media_id', [
            '@media_id' => $source_media->id(),
          ]);
          return FALSE;
        }
      }
      elseif ($field_type === 'entity_reference' && $field_definition->getSetting('target_type') === 'media') {
        // For media reference fields, reference the media entity directly
        $entity->set($field_name, $source_media->id());
        
        $this->logger->info('Reused media @media_id for media reference field @field', [
          '@media_id' => $source_media->id(),
          '@field' => $field_name,
        ]);
      }
      else {
        $this->logger->error('Unsupported field type @type for field @field.', [
          '@type' => $field_type,
          '@field' => $field_name,
        ]);
        return FALSE;
      }
      
      $entity->save();
      
      $this->logger->notice('Successfully reused dynamic image file for @entity_type/@id field @field.', [
        '@entity_type' => $entity->getEntityTypeId(),
        '@id' => $entity->id(),
        '@field' => $field_name,
      ]);
      
      return TRUE;
    }
    catch (\Exception $e) {
      $this->logger->error('Error reusing file for target field: @error', ['@error' => $e->getMessage()]);
      return FALSE;
    }
  }

  /**
   * Get image field from media entity (helper method).
   */
  protected function getImageFieldFromMedia(Media $media) {
    $media_bundle = $media->bundle();
    $possible_fields = [];
    
    if ($media_bundle === 'dynamic_image') {
      $possible_fields = ['field_dynamic_image', 'field_media_image', 'field_image'];
    } elseif ($media_bundle === 'poster') {
      $possible_fields = ['field_poster_image', 'field_media_image', 'field_image'];
    } elseif ($media_bundle === 'image') {
      $possible_fields = ['field_image', 'field_media_image', 'field_poster_image'];
    } else {
      $possible_fields = ['field_media_image', 'field_image', 'field_poster_image', 'field_dynamic_image'];
    }
    
    foreach ($possible_fields as $field_name) {
      if ($media->hasField($field_name) && !$media->get($field_name)->isEmpty()) {
        return $media->get($field_name);
      }
    }
    
    return NULL;
  }

  /**
   * Get image URL from media entity with dynamic field detection.
   */
  protected function getImageUrlFromMedia(Media $media) {
    try {
      $media_bundle = $media->bundle();
      $this->logger->info('Getting image URL from media @media_id (bundle: @bundle)', [
        '@media_id' => $media->id(),
        '@bundle' => $media_bundle,
      ]);
      
      // Try different possible image field names based on bundle
      $possible_fields = [];
      if ($media_bundle === 'dynamic_image') {
        $possible_fields = ['field_dynamic_image', 'field_media_image', 'field_image'];
      } elseif ($media_bundle === 'poster') {
        $possible_fields = ['field_poster_image', 'field_media_image', 'field_image'];
      } elseif ($media_bundle === 'image') {
        $possible_fields = ['field_image', 'field_media_image', 'field_poster_image'];
      } else {
        // For other bundles, try common field names
        $possible_fields = ['field_media_image', 'field_image', 'field_poster_image', 'field_dynamic_image'];
      }
      
      $image_field = NULL;
      $used_field = NULL;
      
      foreach ($possible_fields as $field_name) {
        if ($media->hasField($field_name) && !$media->get($field_name)->isEmpty()) {
          $image_field = $media->get($field_name);
          $used_field = $field_name;
          break;
        }
      }
      
      if (!$image_field) {
        // Log all available fields for debugging
        $available_fields = [];
        foreach ($media->getFieldDefinitions() as $field_name => $field_definition) {
          if ($field_definition->getType() === 'image') {
            $available_fields[] = $field_name;
          }
        }
        
        $this->logger->error('No image field found in media @media_id (bundle: @bundle). Available image fields: @fields', [
          '@media_id' => $media->id(),
          '@bundle' => $media_bundle,
          '@fields' => implode(', ', $available_fields),
        ]);
        return NULL;
      }
      
      $this->logger->info('Using image field @field from media @media_id', [
        '@field' => $used_field,
        '@media_id' => $media->id(),
      ]);
      
      $file = $image_field->entity;
      if ($file) {
        $file_url = $file->createFileUrl();
        
        if ($file_url && strpos($file_url, 'http') !== 0) {
          $request = \Drupal::request();
          $base_url = $request->getSchemeAndHttpHost();
          $file_url = $base_url . $file_url;
        }
        
        $this->logger->info('Generated public URL: @url for file @file_id', [
          '@url' => $file_url,
          '@file_id' => $file->id(),
        ]);
        
        return $file_url;
      }
      
      return NULL;
    }
    catch (\Exception $e) {
      $this->logger->error('Error getting image URL from media: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Save media to entity field with dynamic field detection.
   */
  protected function saveMediaToEntity(EntityInterface $entity, $field_name, Media $media) {
    try {
      if (!$entity->hasField($field_name)) {
        $this->logger->error('Entity @type @id does not have field @field.', [
          '@type' => $entity->getEntityTypeId(),
          '@id' => $entity->id(),
          '@field' => $field_name,
        ]);
        return FALSE;
      }
      
      $field_definition = $entity->getFieldDefinition($field_name);
      $field_type = $field_definition->getType();
      
      $this->logger->info('Saving media @media_id to @field_type field @field on entity @entity_id', [
        '@media_id' => $media->id(),
        '@field_type' => $field_type,
        '@field' => $field_name,
        '@entity_id' => $entity->id(),
      ]);
      
      if ($field_type === 'image') {
        // For image fields, reference the file directly from the media
        $media_bundle = $media->bundle();
        $possible_fields = [];
        if ($media_bundle === 'dynamic_image') {
          $possible_fields = ['field_dynamic_image', 'field_media_image', 'field_image'];
        } elseif ($media_bundle === 'poster') {
          $possible_fields = ['field_poster_image', 'field_media_image', 'field_image'];
        } elseif ($media_bundle === 'image') {
          $possible_fields = ['field_image', 'field_media_image', 'field_poster_image'];
        } else {
          $possible_fields = ['field_media_image', 'field_image', 'field_poster_image', 'field_dynamic_image'];
        }
        
        $image_field = NULL;
        $used_media_field = NULL;
        
        foreach ($possible_fields as $media_field_name) {
          if ($media->hasField($media_field_name) && !$media->get($media_field_name)->isEmpty()) {
            $image_field = $media->get($media_field_name);
            $used_media_field = $media_field_name;
            break;
          }
        }
        
        if (!$image_field) {
          $this->logger->error('No valid image field found in media @media_id for bundle @bundle', [
            '@media_id' => $media->id(),
            '@bundle' => $media_bundle,
          ]);
          return FALSE;
        }
        
        $file_id = $image_field->target_id;
        $alt_text = $image_field->alt ?: 'Generated dynamic image';
        
        $entity->set($field_name, [
          'target_id' => $file_id,
          'alt' => $alt_text,
        ]);
        
        $this->logger->info('Saved media file @file_id to image field @field on entity @entity_id', [
          '@file_id' => $file_id,
          '@field' => $field_name,
          '@entity_id' => $entity->id(),
        ]);
      }
      elseif ($field_type === 'entity_reference' && $field_definition->getSetting('target_type') === 'media') {
        // For media reference fields, reference the media entity directly
        $entity->set($field_name, $media->id());
        
        $this->logger->info('Saved media @media_id to media reference field @field on entity @entity_id', [
          '@media_id' => $media->id(),
          '@field' => $field_name,
          '@entity_id' => $entity->id(),
        ]);
      }
      else {
        $this->logger->error('Unsupported field type @type for field @field.', [
          '@type' => $field_type,
          '@field' => $field_name,
        ]);
        return FALSE;
      }
      
      $entity->save();
      
      $this->logger->notice('Successfully saved media @media_id to @entity_type/@id field @field.', [
        '@media_id' => $media->id(),
        '@entity_type' => $entity->getEntityTypeId(),
        '@id' => $entity->id(),
        '@field' => $field_name,
      ]);
      
      return TRUE;
    }
    catch (\Exception $e) {
      $this->logger->error('Error saving media to entity field: @error', ['@error' => $e->getMessage()]);
      return FALSE;
    }
  }

  /**
   * Calls the API to generate a poster image.
   */
  protected function callPosterGenerationApi($html, $css, $options = []) {
    try {
      // Get configuration settings
      $config = $this->configFactory->get('dynamic_image_generator.settings');
      $api_provider = $config->get('api_provider') ?: 'htmlcsstoimage';

      // If using inbuilt generator, call that service instead
      if ($api_provider === 'inbuilt') {
        if (\Drupal::hasService('image_creating_engine.generator')) {
          $generator = \Drupal::service('image_creating_engine.generator');
          $result = $generator->generateImage($html, $css, $options);
          
          // Return the result directly - no need to download anything
          return $result;
        } else {
          $this->logger->error('Inbuilt image generator service not available. Make sure the image_creating_engine module is enabled.');
          return NULL;
        }
      }

      // For external API, validate credentials and make API call
      $api_user_id = $config->get('api_user_id');
      $api_key = $config->get('api_key');
      $api_endpoint = $config->get('api_endpoint') ?: 'https://hcti.io/v1/image';

      if (empty($api_user_id) || empty($api_key)) {
        $this->logger->error('API credentials not configured. Please set API User ID and API Key in the module settings.');
        return NULL;
      }

      // Default options
      $width = $options['width'] ?? $config->get('default_width') ?? 1200;
      $height = $options['height'] ?? $config->get('default_height') ?? 630;
      
      $data = [
        'html' => $html,
        'css' => $css,
        'width' => $width,
        'height' => $height,
      ];
      
      $format = $config->get('image_format');
      if ($format && $format !== 'png') {
        $data['format'] = $format;
        
        if (in_array($format, ['jpg', 'jpeg', 'webp']) && $config->get('quality')) {
          $data['quality'] = (int) $config->get('quality');
        }
      }
      
      if ($config->get('debug_mode')) {
        $this->logger->info('Sending API request to @endpoint with data: @data', [
          '@endpoint' => $api_endpoint,
          '@data' => json_encode($data, JSON_PRETTY_PRINT),
        ]);
      }
      
      $attempt = 0;
      $max_attempts = ($config->get('retry_attempts') ?: 2) + 1;
      
      while ($attempt < $max_attempts) {
        $attempt++;
        
        try {
          $start_time = microtime(TRUE);
          
          $response = $this->httpClient->request('POST', $api_endpoint, [
            'form_params' => $data,
            'auth' => [$api_user_id, $api_key],
            'timeout' => $config->get('request_timeout') ?: 60,
            'headers' => [
              'Content-Type' => 'application/x-www-form-urlencoded',
              'User-Agent' => 'Drupal-DynamicImageGenerator/1.0',
            ],
          ]);
          
          $response_time = round((microtime(TRUE) - $start_time) * 1000, 2);
          
          if ($response->getStatusCode() === 200) {
            $result = json_decode($response->getBody(), TRUE);
            
            if ($config->get('debug_mode')) {
              $this->logger->info('API response received in @time ms: @response', [
                '@time' => $response_time,
                '@response' => json_encode($result, JSON_PRETTY_PRINT),
              ]);
            }
            
            if (isset($result['url'])) {
              $this->logger->info('Successfully generated image in @time ms: @url', [
                '@time' => $response_time,
                '@url' => $result['url'],
              ]);
              return $result['url'];
            } else {
              $this->logger->error('API did not return image URL. Response: @response', [
                '@response' => substr($response->getBody(), 0, 500),
              ]);
              return NULL;
            }
          }
        }
        catch (\Exception $e) {
          $this->logger->error('API call failed on attempt @attempt/@max: @error', [
            '@attempt' => $attempt,
            '@max' => $max_attempts,
            '@error' => $e->getMessage(),
          ]);
          
          if ($attempt < $max_attempts) {
            sleep(1);
          }
        }
      }
      
      $this->logger->error('All @max API call attempts failed', [
        '@max' => $max_attempts,
      ]);
      
      return NULL;
    }
    catch (\Exception $e) {
      $this->logger->error('Error calling HTML/CSS to Image API: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Downloads and saves an image from a URL.
   *
   * @param string|array $image_url_or_data
   *   The URL of the image to download or array with image data.
   * @param int|null $entity_id
   *   Optional entity ID for naming.
   *
   * @return \Drupal\file\FileInterface|null
   *   The created file entity or NULL on failure.
   */
  protected function downloadAndSaveImageFile($image_url_or_data, $entity_id = NULL) {
    try {
      // Check if this is already a processed file from inbuilt generator
      if (is_array($image_url_or_data) && isset($image_url_or_data['file']) && $image_url_or_data['file'] instanceof \Drupal\file\FileInterface) {
        $this->logger->info('Using pre-created file entity from inbuilt generator');
        return $image_url_or_data['file'];
      }
      
      // Skip downloading if not a URL string
      if (!is_string($image_url_or_data)) {
        $this->logger->error('Invalid image URL format: ' . gettype($image_url_or_data));
        return NULL;
      }

      $image_url = $image_url_or_data;
      
      // Create the directory
      $directory = 'public://dynamic_image_generator/generated';
      $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
      
      // Try to get the file contents
      $file_data = @file_get_contents($image_url);
      if ($file_data === FALSE) {
        $error = error_get_last();
        $this->logger->error('Failed to download image from URL @url: @error', [
          '@url' => $image_url,
          '@error' => isset($error['message']) ? $error['message'] : 'Unknown error',
        ]);
        return NULL;
      }
      
      // Create a meaningful filename
      $entity_suffix = $entity_id ? "_entity_{$entity_id}" : '';
      $file_name = 'dynamic_image' . $entity_suffix . '_' . md5($image_url . time()) . '.png';
      $uri = $directory . '/' . $file_name;
      
      // Save the file data to disk first
      if (!file_put_contents($uri, $file_data)) {
        $this->logger->error('Failed to write file data to @uri', ['@uri' => $uri]);
        return NULL;
      }

      // Create a managed file entity
      $file = File::create([
        'uri' => $uri,
        'filename' => $file_name,
        'status' => 1, // FILE_STATUS_PERMANENT = 1
        'uid' => \Drupal::currentUser()->id(),
      ]);
      $file->save();
      
      $this->logger->info('Successfully downloaded and saved image to @uri', ['@uri' => $uri]);
      return $file;
    }
    catch (\Exception $e) {
      $this->logger->error('Failed to download and save image file: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Downloads an image from a URL and creates a file entity.
   * Only used for external API results, not for inbuilt generator.
   */
  protected function downloadImageFile($image_url) {
    try {
      // IMPORTANT: Check if this is already a processed file from inbuilt generator
      if (is_array($image_url) && isset($image_url['file']) && $image_url['file'] instanceof \Drupal\file\FileInterface) {
        $this->logger->info('Using pre-created file entity from inbuilt generator');
        return $image_url['file'];
      }
      
      // Skip downloading if not a URL string
      if (!is_string($image_url)) {
        $this->logger->error('Invalid image URL format: ' . gettype($image_url));
        return NULL;
      }

      // Regular download process for URL strings
      $directory = 'public://dynamic_image_generator/generated';
      $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
      
      $file_data = file_get_contents($image_url);
      if ($file_data === FALSE) {
        $this->logger->error('Failed to download image from URL @url', ['@url' => $image_url]);
        return NULL;
      }
      
      $file_name = 'dynamic_image_' . md5($image_url . time()) . '.png';
      $uri = $directory . '/' . $file_name;
      
      $file = file_save_data($file_data, $uri, FileSystemInterface::EXISTS_REPLACE);
      if (!$file) {
        $this->logger->error('Failed to save downloaded image data to @uri', ['@uri' => $uri]);
        return NULL;
      }
      
      return $file;
    }
    catch (\Exception $e) {
      $this->logger->error('Failed to download image file: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Creates a media entity from a generated image.
   */
  protected function createMediaEntity($image_data, $template_id, $entity_id = NULL) {
    try {
      $template = $this->entityTypeManager->getStorage('poster_entity')->load($template_id);
      if (!$template) {
        $this->logger->error('Failed to load template with ID @id', ['@id' => $template_id]);
        return NULL;
      }

      // Handle different formats of $image_data based on the source
      $file = NULL;
      
      // Check if we received data from the inbuilt generator (array with file entity)
      if (is_array($image_data) && isset($image_data['file']) && $image_data['file'] instanceof \Drupal\file\FileInterface) {
        // For inbuilt generator - use the file entity directly, no download needed
        $file = $image_data['file'];
        $this->logger->info('Using pre-created file entity from inbuilt generator');
      }
      // If we got a URL string from an external API, download it
      elseif (is_string($image_data)) {
        // For external API - download the file from URL
        $file = $this->downloadImageFile($image_data);
        if (!$file) {
          $this->logger->error('Failed to download image from URL @url', ['@url' => $image_data]);
          return NULL;
        }
      }
      else {
        $this->logger->error('Invalid image data format received: ' . gettype($image_data));
        return NULL;
      }

      // Create media entity with the file
      $media_bundle = 'dynamic_image';
      $image_field_name = 'field_dynamic_image';
      
      // Check if we have the dynamic_image media type
      $media_type_storage = $this->entityTypeManager->getStorage('media_type');
      $available_types = $media_type_storage->loadMultiple();
      
      if (isset($available_types['dynamic_image'])) {
        $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
        foreach ($bundle_fields as $field_name => $field_definition) {
          if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
            $image_field_name = $field_name;
            break;
          }
        }
        $this->logger->info('Using dynamic_image media type with field @field for dynamic image', ['@field' => $image_field_name]);
      } else {
        // Fallback to image media type if dynamic_image doesn't exist
        if (isset($available_types['image'])) {
          $media_bundle = 'image';
          $image_field_name = 'field_media_image';
          $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'image');
          foreach ($bundle_fields as $field_name => $field_definition) {
            if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
              $image_field_name = $field_name;
              break;
            }
          }
          $this->logger->info('dynamic_image media type not found, using image media type with field @field', ['@field' => $image_field_name]);
        } else {
          // Use first available media type as last resort
          $first_type = reset($available_types);
          if ($first_type) {
            $media_bundle = $first_type->id();
            $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', $media_bundle);
            foreach ($bundle_fields as $field_name => $field_definition) {
              if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
                $image_field_name = $field_name;
                break;
              }
            }
          }
          $this->logger->warning('No dynamic_image or image media types found, using @bundle with field @field', [
            '@bundle' => $media_bundle,
            '@field' => $image_field_name,
          ]);
        }
      }

      // Create dynamic image media entity with proper metadata
      $media_data = [
        'bundle' => $media_bundle,
        'name' => 'Dynamic Image - ' . ($entity_id ? "Entity $entity_id" : 'Preview') . ' - ' . date('Y-m-d H:i:s'),
        $image_field_name => [
          'target_id' => $file->id(),
          'alt' => 'Generated dynamic image - ' . date('Y-m-d H:i:s'),
        ],
        'uid' => \Drupal::currentUser()->id(),
      ];
      
      // Add custom fields if they exist for dynamic_image media type
      if ($media_bundle === 'dynamic_image') {
        $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
        
        // Add template reference if field exists
        if (isset($bundle_fields['field_template_id'])) {
          $media_data['field_template_id'] = $poster_entity_id;
        }
        
        // Add source entity reference if field exists  
        if (isset($bundle_fields['field_source_entity']) && $entity_id) {
          $media_data['field_source_entity'] = $entity_id;
        }
      }
      
      $media = Media::create($media_data);
      $media->save();
      
      $this->logger->notice('Created dynamic image media entity @media_id (bundle: @bundle) for poster @poster_id and entity @entity_id with file @file_id', [
        '@media_id' => $media->id(),
        '@bundle' => $media_bundle,
        '@poster_id' => $poster_entity_id,
        '@entity_id' => $entity_id,
        '@file_id' => $file->id(),
      ]);
      
      return $media;
    }
    catch (\Exception $e) {
      $this->logger->error('Failed to create dynamic image media entity: @error', ['@error' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Process Drupal tokens in the given text.
   */
  protected function processTokens($text, array $data = []) {
    if (empty($text) || empty($data)) {
      return $text;
    }
    
    return $this->token->replace($text, $data, [
      'clear' => TRUE,
      'sanitize' => FALSE,
    ]);
  }

  /**
   * Prepare image tokens from a poster entity's images.
   */
  protected function prepareImageTokens($poster_entity) {
    $image_tokens = [];
    
    $images = $poster_entity->getBackgroundImages();
    
    foreach ($images as $index => $file) {
      if ($file instanceof FileInterface) {
        $token_name = 'image_' . ($index + 1);
        $image_tokens[$token_name] = $file->createFileUrl();
      }
    }
    
    if (!empty($images)) {
      $random_index = array_rand($images);
      $random_file = $images[$random_index];
      if ($random_file instanceof FileInterface) {
        $image_tokens['image_random'] = $random_file->createFileUrl();
      }
    }
    
    return $image_tokens;
  }

  /**
   * Replace image tokens in a template.
   */
  protected function replaceImageTokens($template, array $image_tokens) {
    foreach ($image_tokens as $token => $url) {
      $template = str_replace('[' . $token . ']', $url, $template);
    }
    
    return $template;
  }

  /**
   * Render a string as a Twig template with the given context.
   */
  protected function renderTwigTemplate($template_string, array $context = []) {
    try {
      $template_hash = md5($template_string . microtime());
      $template_name = "poster_template_{$template_hash}";
      
      $template_string = "{% autoescape false %}{$template_string}{% endautoescape %}";
      
      $template = $this->twig->createTemplate($template_string, $template_name);
      
      return $template->render($context);
    } 
    catch (TwigError $e) {
      $this->logger->error('Twig template error: @error', ['@error' => $e->getMessage()]);
      return $template_string;
    }
  }

  /**
   * Sanitize content to make sure it's properly formatted.
   */
  protected function sanitizeContent($content) {
    $content = trim($content);
    $content = str_replace(["\r\n", "\r"], "\n", $content);
    
    return $content;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc