PluginProbe
Auto Alt Text / trunk
Auto Alt Text vtrunk
3.0.3 2.8.2 1.3.1 1.3.2 2.0.0 2.1.0 2.1.1 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.7.0 2.8.0 2.8.1 All 28 releases
auto-alt-text / src / App / Services / AltTextService.php

AltTextService.php in Auto Alt Text trunk, at src/App/Services/AltTextService.php

355 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace AATXT\App\Services;
4
5 use AATXT\App\Admin\PluginOptions;
6 use AATXT\App\AltTextGeneratorAi;
7 use AATXT\App\AltTextGeneratorAttachmentTitle;
8 use AATXT\App\Domain\Exceptions\UnsupportedGeneratorException;
9 use AATXT\App\Events\AltTextGeneratedEvent;
10 use AATXT\App\Events\AltTextGenerationFailedEvent;
11 use AATXT\App\Events\EventDispatcherInterface;
12 use AATXT\App\Exceptions\Anthropic\AnthropicException;
13 use AATXT\App\Exceptions\Azure\AzureException;
14 use AATXT\App\Exceptions\Gemini\GeminiException;
15 use AATXT\App\Exceptions\OpenAI\OpenAIException;
16 use AATXT\App\Infrastructure\Repositories\ConfigRepositoryInterface;
17 use AATXT\App\Infrastructure\Repositories\ErrorLogRepositoryInterface;
18 use AATXT\Config\Constants;
19 use Exception;
20
21 /**
22 * Service for generating alt text for WordPress attachments.
23 *
24 * This service encapsulates all the business logic for alt text generation,
25 * using the Factory pattern to create appropriate generators and handling
26 * error logging.
27 *
28 * If the selected AI provider fails, the error is logged and no alt text is generated.
29 */
30 final class AltTextService
31 {
32 /**
33 * Factory for creating alt text generators
34 *
35 * @var AltTextGeneratorFactory
36 */
37 private $factory;
38
39 /**
40 * Configuration repository
41 *
42 * @var ConfigRepositoryInterface
43 */
44 private $config;
45
46 /**
47 * Error log repository
48 *
49 * @var ErrorLogRepositoryInterface
50 */
51 private $errorLog;
52
53 /**
54 * MIME type validation map
55 *
56 * @var array<string, array<string>>
57 */
58 private $allowedMimeTypes;
59
60 /**
61 * Event dispatcher for publishing events (optional)
62 *
63 * @var EventDispatcherInterface|null
64 */
65 private $eventDispatcher;
66
67 /**
68 * Constructor
69 *
70 * @param AltTextGeneratorFactory $factory Factory for creating generators
71 * @param ConfigRepositoryInterface $config Configuration repository
72 * @param ErrorLogRepositoryInterface $errorLog Error logging repository
73 * @param EventDispatcherInterface|null $eventDispatcher Event dispatcher (optional)
74 */
75 public function __construct(
76 AltTextGeneratorFactory $factory,
77 ConfigRepositoryInterface $config,
78 ErrorLogRepositoryInterface $errorLog,
79 ?EventDispatcherInterface $eventDispatcher = null
80 ) {
81 $this->factory = $factory;
82 $this->config = $config;
83 $this->errorLog = $errorLog;
84 $this->eventDispatcher = $eventDispatcher;
85
86 // Map of allowed MIME types per generator type
87 $this->allowedMimeTypes = [
88 Constants::AATXT_OPTION_TYPOLOGY_CHOICE_OPENAI => Constants::AATXT_OPENAI_ALLOWED_MIME_TYPES,
89 Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ANTHROPIC => Constants::AATXT_ANTHROPIC_ALLOWED_MIME_TYPES,
90 Constants::AATXT_OPTION_TYPOLOGY_CHOICE_GEMINI => Constants::AATXT_GEMINI_ALLOWED_MIME_TYPES,
91 Constants::AATXT_OPTION_TYPOLOGY_CHOICE_AZURE => Constants::AATXT_AZURE_ALLOWED_MIME_TYPES,
92 ];
93 }
94
95 /**
96 * Set the event dispatcher.
97 *
98 * @param EventDispatcherInterface $eventDispatcher The event dispatcher
99 * @return self Fluent interface
100 */
101 public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): self
102 {
103 $this->eventDispatcher = $eventDispatcher;
104 return $this;
105 }
106
107 /**
108 * Generate alt text for a WordPress attachment.
109 *
110 * This method orchestrates the entire alt text generation process:
111 * - Validates the attachment is an image
112 * - Checks if existing alt text should be preserved
113 * - Validates MIME type for AI providers
114 * - Creates appropriate generator via factory
115 * - Handles errors and logging
116 *
117 * If the selected provider fails, the error is logged and an empty string is returned.
118 *
119 * @param int $attachmentId WordPress attachment post ID
120 * @return string Generated alt text, or empty string if generation fails
121 */
122 public function generateForAttachment(int $attachmentId): string
123 {
124 // Verify it's an image attachment
125 if (!wp_attachment_is_image($attachmentId)) {
126 return '';
127 }
128
129 // Check if we should preserve existing alt text
130 if (PluginOptions::preserveExistingAltText()) {
131 $existingAltText = get_post_meta($attachmentId, '_wp_attachment_image_alt', true);
132
133 if (!empty($existingAltText)) {
134 return $existingAltText;
135 }
136 }
137
138 $typology = PluginOptions::typology();
139
140 // Deactivated - return empty
141 if ($typology === Constants::AATXT_OPTION_TYPOLOGY_DEACTIVATED) {
142 return '';
143 }
144
145 // Special handling for article title typology (has fallback logic to attachment title)
146 if ($typology === Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ARTICLE_TITLE) {
147 return $this->generateFromArticleTitle($attachmentId);
148 }
149
150 // Validate MIME type for AI providers
151 if ($this->requiresMimeTypeValidation($typology)) {
152 $mimeType = get_post_mime_type($attachmentId);
153 if (!$this->isMimeTypeSupported($typology, $mimeType)) {
154 $this->logMimeTypeError($attachmentId, $typology);
155 return '';
156 }
157 }
158
159 // Generate alt text using the factory
160 try {
161 $generator = $this->factory->create($typology);
162 $altText = $generator->altText($attachmentId);
163
164 // Dispatch success event
165 $this->dispatchSuccessEvent($attachmentId, $altText, $typology);
166
167 return $altText;
168 } catch (UnsupportedGeneratorException $e) {
169 // Typology not registered in factory
170 return '';
171 } catch (OpenAIException $e) {
172 $this->handleFailure($attachmentId, 'OpenAI', $e);
173 return '';
174 } catch (AnthropicException $e) {
175 $this->handleFailure($attachmentId, 'Anthropic', $e);
176 return '';
177 } catch (GeminiException $e) {
178 $this->handleFailure($attachmentId, 'Gemini', $e);
179 return '';
180 } catch (AzureException $e) {
181 $this->handleFailure($attachmentId, 'Azure', $e);
182 return '';
183 } catch (Exception $e) {
184 $this->handleFailure($attachmentId, 'Unknown', $e);
185 return '';
186 }
187 }
188
189 /**
190 * Generate alt text from article title with fallback to attachment title.
191 *
192 * @param int $attachmentId WordPress attachment ID
193 * @return string Generated alt text
194 */
195 private function generateFromArticleTitle(int $attachmentId): string
196 {
197 $parentId = wp_get_post_parent_id($attachmentId);
198
199 if ($parentId) {
200 // Has parent post - use parent post title
201 try {
202 $generator = $this->factory->create(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ARTICLE_TITLE);
203 return $generator->altText($attachmentId);
204 } catch (Exception $e) {
205 // Fallback to attachment title if parent title fails
206 return $this->generateFromAttachmentTitle($attachmentId);
207 }
208 }
209
210 // No parent post - use attachment title as fallback
211 return $this->generateFromAttachmentTitle($attachmentId);
212 }
213
214 /**
215 * Generate alt text from attachment title.
216 *
217 * @param int $attachmentId WordPress attachment ID
218 * @return string Generated alt text
219 */
220 private function generateFromAttachmentTitle(int $attachmentId): string
221 {
222 try {
223 $generator = $this->factory->create(Constants::AATXT_OPTION_TYPOLOGY_CHOICE_ATTACHMENT_TITLE);
224 return $generator->altText($attachmentId);
225 } catch (Exception $e) {
226 return '';
227 }
228 }
229
230 /**
231 * Check if a typology requires MIME type validation.
232 *
233 * @param string $typology Generator typology
234 * @return bool True if MIME validation required
235 */
236 private function requiresMimeTypeValidation(string $typology): bool
237 {
238 return isset($this->allowedMimeTypes[$typology]);
239 }
240
241 /**
242 * Check if a MIME type is supported for the given typology.
243 *
244 * @param string $typology Generator typology
245 * @param string $mimeType MIME type to check
246 * @return bool True if supported
247 */
248 private function isMimeTypeSupported(string $typology, string $mimeType): bool
249 {
250 $allowedTypes = $this->allowedMimeTypes[$typology] ?? [];
251 return in_array($mimeType, $allowedTypes, true);
252 }
253
254 /**
255 * Log MIME type validation error.
256 *
257 * @param int $attachmentId WordPress attachment ID
258 * @param string $typology Generator typology
259 * @return void
260 */
261 private function logMimeTypeError(int $attachmentId, string $typology): void
262 {
263 $allowedTypes = $this->allowedMimeTypes[$typology] ?? [];
264 $formats = $this->formatMimeTypeList($allowedTypes);
265 $message = "You uploaded an unsupported image. Please make sure your image has one of the following formats: $formats";
266
267 $this->logError($attachmentId, $typology, $message);
268 }
269
270 /**
271 * Format MIME type list for display.
272 *
273 * @param array<string> $mimeTypes List of MIME types
274 * @return string Formatted string (e.g., "png, jpeg, gif")
275 */
276 private function formatMimeTypeList(array $mimeTypes): string
277 {
278 return str_replace('image/', '', implode(', ', $mimeTypes));
279 }
280
281 /**
282 * Log an error to the error repository.
283 *
284 * @param int $imageId WordPress attachment ID
285 * @param string $provider Provider name
286 * @param string $message Error message
287 * @return void
288 */
289 private function logError(int $imageId, string $provider, string $message): void
290 {
291 $errorMessage = $provider . ' - ' . $message;
292
293 $errorLog = new \AATXT\App\Domain\Entities\ErrorLog($imageId, $errorMessage);
294 $this->errorLog->save($errorLog);
295 }
296
297 /**
298 * Dispatch an event if the event dispatcher is available.
299 *
300 * @param object $event The event to dispatch
301 * @return void
302 */
303 private function dispatchEvent(object $event): void
304 {
305 if ($this->eventDispatcher !== null) {
306 $this->eventDispatcher->dispatch($event);
307 }
308 }
309
310 /**
311 * Dispatch a success event after alt text generation.
312 *
313 * @param int $imageId WordPress attachment ID
314 * @param string $altText Generated alt text
315 * @param string $provider Provider name
316 * @return void
317 */
318 private function dispatchSuccessEvent(int $imageId, string $altText, string $provider): void
319 {
320 if ($altText !== '') {
321 $this->dispatchEvent(new AltTextGeneratedEvent($imageId, $altText, $provider));
322 }
323 }
324
325 /**
326 * Handle a generation failure, making sure it is persisted exactly once.
327 *
328 * The error is normally logged by the listeners subscribed to
329 * AltTextGenerationFailedEvent (see LogErrorListener). We only write to the
330 * repository ourselves when nothing else did: either because no dispatcher
331 * is configured, or because no subscribed listener marked the event as
332 * handled. Logging here unconditionally would duplicate every row in the
333 * error log.
334 *
335 * @param int $imageId WordPress attachment ID
336 * @param string $provider Provider name
337 * @param Exception $exception The exception that caused the failure
338 * @return void
339 */
340 private function handleFailure(int $imageId, string $provider, Exception $exception): void
341 {
342 if ($this->eventDispatcher === null) {
343 $this->logError($imageId, $provider, $exception->getMessage());
344 return;
345 }
346
347 $event = new AltTextGenerationFailedEvent($imageId, $provider, $exception);
348 $this->eventDispatcher->dispatch($event);
349
350 if (!$event->isHandled()) {
351 $this->logError($imageId, $provider, $exception->getMessage());
352 }
353 }
354 }
355