PluginProbe
Auto Alt Text / 2.7.0
Auto Alt Text v2.7.0
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 2.7.0, at src/App/Services/AltTextService.php

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