| @@ -1,10 +1,11 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | 3 | * Cloud-print provider capabilities value object. |
| 4 | 4 | * |
| 5 | - * Single source of truth for per-provider knowledge: validity, polling, | |
| 6 | - * content types, poll endpoints, server diagnostics and thermal wire formats. | |
| 5 | + * Single source of truth for per-provider knowledge: validity, the default for | |
| 6 | + * rows without a provider, polling, content types, poll endpoints, server | |
| 7 | + * diagnostics, thermal wire formats and renderable template engines. | |
| 7 | 8 | * |
| 8 | 9 | * @package WCPOS\WooCommercePOS\Services |
| 9 | 10 | */ |
| 10 | 11 | |
| @@ -9,13 +10,28 @@ | ||
| 9 | 10 | */ |
| 10 | 11 | |
| 11 | 12 | namespace WCPOS\WooCommercePOS\Services; |
| 12 | 13 | |
| 14 | +use WCPOS\WooCommercePOS\Interfaces\Provider_Adapter_Interface; | |
| 15 | +use WCPOS\WooCommercePOS\Interfaces\Poll_Provider_Adapter_Interface; | |
| 16 | +use WCPOS\WooCommercePOS\Services\Providers\Epson_Sdp_Adapter; | |
| 17 | +use WCPOS\WooCommercePOS\Services\Providers\Printnode_Adapter; | |
| 18 | +use WCPOS\WooCommercePOS\Services\Providers\Star_Cloudprnt_Adapter; | |
| 19 | +use WCPOS\WooCommercePOS\Services\Providers\Star_Online_Adapter; | |
| 20 | + | |
| 13 | 21 | /** |
| 14 | 22 | * Provider class. |
| 15 | 23 | */ |
| 16 | 24 | class Provider { |
| 17 | 25 | /** |
| 26 | + * Provider assumed for printer rows that predate the provider field. | |
| 27 | + * | |
| 28 | + * Star CloudPRNT was the only provider before the field existed, so a row | |
| 29 | + * without one is a Star CloudPRNT printer. | |
| 30 | + */ | |
| 31 | + public const DEFAULT_PROVIDER = 'star-cloudprnt'; | |
| 32 | + | |
| 33 | + /** | |
| 18 | 34 | * Per-provider capability map. |
| 19 | 35 | * |
| 20 | 36 | * @var array<string, array<string, mixed>> |
| 21 | 37 | */ |
| @@ -28,8 +44,14 @@ | ||
| 28 | 44 | 'content_type' => 'application/vnd.star.starprnt', |
| 29 | 45 | 'poll_endpoint' => 'cloudprnt', |
| 30 | 46 | 'supports_server_diagnostic' => true, |
| 31 | 47 | 'thermal_wire_format' => 'starprnt', |
| 48 | + 'template_engines' => 'thermal', | |
| 49 | + 'stores_job_kind' => false, | |
| 50 | + // The StarPRNT emitter emits a native drawer pulse (and inserts one | |
| 51 | + // before the trailing cut when a job asks for it), so drawer metadata | |
| 52 | + // has to survive to render time. | |
| 53 | + 'supports_drawer' => true, | |
| 32 | 54 | ), |
| 33 | 55 | 'epson-sdp' => array( |
| 34 | 56 | 'polling' => true, |
| 35 | 57 | 'content_type' => 'application/xml', |
| @@ -35,8 +57,11 @@ | ||
| 35 | 57 | 'content_type' => 'application/xml', |
| 36 | 58 | 'poll_endpoint' => 'epson-sdp', |
| 37 | 59 | 'supports_server_diagnostic' => true, |
| 38 | 60 | 'thermal_wire_format' => 'epos-xml', |
| 61 | + 'template_engines' => 'thermal', | |
| 62 | + 'stores_job_kind' => false, | |
| 63 | + 'supports_drawer' => true, | |
| 39 | 64 | ), |
| 40 | 65 | 'printnode' => array( |
| 41 | 66 | 'polling' => false, |
| 42 | 67 | 'content_type' => 'application/pdf', |
| @@ -42,8 +67,11 @@ | ||
| 42 | 67 | 'content_type' => 'application/pdf', |
| 43 | 68 | 'poll_endpoint' => null, |
| 44 | 69 | 'supports_server_diagnostic' => false, |
| 45 | 70 | 'thermal_wire_format' => null, |
| 71 | + 'template_engines' => 'all', | |
| 72 | + 'stores_job_kind' => true, | |
| 73 | + 'supports_drawer' => true, | |
| 46 | 74 | ), |
| 47 | 75 | 'star-online' => array( |
| 48 | 76 | 'polling' => false, |
| 49 | 77 | 'content_type' => 'text/vnd.star.markup', |
| @@ -49,8 +77,11 @@ | ||
| 49 | 77 | 'content_type' => 'text/vnd.star.markup', |
| 50 | 78 | 'poll_endpoint' => null, |
| 51 | 79 | 'supports_server_diagnostic' => false, |
| 52 | 80 | 'thermal_wire_format' => 'star-markup', |
| 81 | + 'template_engines' => 'thermal', | |
| 82 | + 'stores_job_kind' => false, | |
| 83 | + 'supports_drawer' => false, | |
| 53 | 84 | ), |
| 54 | 85 | ); |
| 55 | 86 | |
| 56 | 87 | /** |
| @@ -62,8 +93,106 @@ | ||
| 62 | 93 | return array_keys( self::CAPABILITIES ); |
| 63 | 94 | } |
| 64 | 95 | |
| 65 | 96 | /** |
| 97 | + * Resolve a stored printer row's provider to a known provider key. | |
| 98 | + * | |
| 99 | + * Callers read `$printer['provider']` from an option that predates the | |
| 100 | + * field, so the value can be missing, empty, or (for hand-edited options) | |
| 101 | + * a key this build does not know. All three resolve to the default rather | |
| 102 | + * than to a silent no-provider state. | |
| 103 | + * | |
| 104 | + * @param string|null $provider Raw provider value from a printer row. | |
| 105 | + * | |
| 106 | + * @return string A key from self::valid(). | |
| 107 | + */ | |
| 108 | + public static function normalize( ?string $provider ): string { | |
| 109 | + return \in_array( $provider, self::valid(), true ) ? (string) $provider : self::DEFAULT_PROVIDER; | |
| 110 | + } | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * Resolve a provider adapter. | |
| 114 | + * | |
| 115 | + * An empty legacy-row value uses normalize()'s Star CloudPRNT default; | |
| 116 | + * non-empty unknown keys remain unknown and return null. | |
| 117 | + * | |
| 118 | + * @param string $provider Provider key or empty legacy-row value. | |
| 119 | + * | |
| 120 | + * @return Provider_Adapter_Interface|null | |
| 121 | + */ | |
| 122 | + public static function adapter( string $provider ): ?Provider_Adapter_Interface { | |
| 123 | + $provider = '' === $provider ? self::normalize( $provider ) : $provider; | |
| 124 | + | |
| 125 | + switch ( $provider ) { | |
| 126 | + case 'star-cloudprnt': | |
| 127 | + return new Star_Cloudprnt_Adapter(); | |
| 128 | + case 'epson-sdp': | |
| 129 | + return new Epson_Sdp_Adapter(); | |
| 130 | + case 'printnode': | |
| 131 | + return new Printnode_Adapter(); | |
| 132 | + case 'star-online': | |
| 133 | + return new Star_Online_Adapter(); | |
| 134 | + default: | |
| 135 | + return null; | |
| 136 | + } | |
| 137 | + } | |
| 138 | + | |
| 139 | + /** | |
| 140 | + * Resolve the wire format and HTTP content type for a print job. | |
| 141 | + * | |
| 142 | + * @param array $printer Printer configuration. | |
| 143 | + * @param array $template Template configuration. | |
| 144 | + * | |
| 145 | + * @return array{kind:string, content_type:string} | |
| 146 | + */ | |
| 147 | + public static function format( array $printer, array $template ): array { | |
| 148 | + // Printer rows saved before the provider field existed have none; they must behave as the default provider. | |
| 149 | + $provider = self::normalize( \is_string( $printer['provider'] ?? null ) ? $printer['provider'] : null ); | |
| 150 | + $adapter = self::adapter( $provider ); | |
| 151 | + if ( null === $adapter ) { | |
| 152 | + return array( | |
| 153 | + 'kind' => '', | |
| 154 | + 'content_type' => '', | |
| 155 | + ); | |
| 156 | + } | |
| 157 | + | |
| 158 | + return $adapter->format( $printer, $template ); | |
| 159 | + } | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * Resolve the HTTP content type for a printer when no template is in hand. | |
| 163 | + * | |
| 164 | + * The reprint path uses this only when the template cannot be rendered and | |
| 165 | + * the job carries no `pn_kind`; otherwise it keeps the stored content type | |
| 166 | + * so the two halves cannot drift apart. | |
| 167 | + * | |
| 168 | + * PrintNode reports its PDF default even for a printer in raw mode. The | |
| 169 | + * reprint path's `pn_kind` condition keeps raw jobs away from this answer. | |
| 170 | + * Prefer format() when a template is in hand to resolve both halves together. | |
| 171 | + * | |
| 172 | + * @param array $printer Printer configuration. | |
| 173 | + * | |
| 174 | + * @return string | |
| 175 | + */ | |
| 176 | + public static function printer_content_type( array $printer ): string { | |
| 177 | + $provider = self::normalize( \is_string( $printer['provider'] ?? null ) ? $printer['provider'] : null ); | |
| 178 | + $adapter = self::adapter( $provider ); | |
| 179 | + | |
| 180 | + return null === $adapter ? 'application/octet-stream' : $adapter->content_type(); | |
| 181 | + } | |
| 182 | + | |
| 183 | + /** | |
| 184 | + * Resolve only polling adapters without widening the base provider contract. | |
| 185 | + * | |
| 186 | + * @param string $key Provider key. | |
| 187 | + * @return Poll_Provider_Adapter_Interface|null | |
| 188 | + */ | |
| 189 | + public static function poll_adapter( string $key ): ?Poll_Provider_Adapter_Interface { | |
| 190 | + $adapter = self::adapter( $key ); | |
| 191 | + return $adapter instanceof Poll_Provider_Adapter_Interface ? $adapter : null; | |
| 192 | + } | |
| 193 | + | |
| 194 | + /** | |
| 66 | 195 | * Whether the provider polls the server for jobs. |
| 67 | 196 | * |
| 68 | 197 | * @param string $provider Provider key. |
| 69 | 198 | * |
| @@ -134,6 +263,66 @@ | ||
| 134 | 263 | return null; |
| 135 | 264 | } |
| 136 | 265 | |
| 137 | 266 | return self::CAPABILITIES[ $provider ]['thermal_wire_format'] ?? null; |
| 267 | + } | |
| 268 | + | |
| 269 | + /** | |
| 270 | + * Receipt-template engines the provider can render for automatic jobs. | |
| 271 | + * | |
| 272 | + * 'all' means every active template; 'thermal' means thermal templates | |
| 273 | + * only. Unknown providers are treated as thermal-only, the conservative | |
| 274 | + * answer for a printer we cannot render a PDF for. | |
| 275 | + * | |
| 276 | + * @param string $provider Provider key. | |
| 277 | + * | |
| 278 | + * @return string 'all' or 'thermal'. | |
| 279 | + */ | |
| 280 | + public static function template_engines( string $provider ): string { | |
| 281 | + return (string) ( self::CAPABILITIES[ $provider ]['template_engines'] ?? 'thermal' ); | |
| 282 | + } | |
| 283 | + | |
| 284 | + /** | |
| 285 | + * Whether jobs for the provider persist their resolved kind separately. | |
| 286 | + * | |
| 287 | + * @param string $provider Provider key. | |
| 288 | + * | |
| 289 | + * @return bool | |
| 290 | + */ | |
| 291 | + public static function stores_job_kind( string $provider ): bool { | |
| 292 | + return (bool) ( self::CAPABILITIES[ $provider ]['stores_job_kind'] ?? false ); | |
| 293 | + } | |
| 294 | + | |
| 295 | + /** | |
| 296 | + * Whether the provider supports the generic drawer metadata contract. | |
| 297 | + * | |
| 298 | + * @param string $provider Provider key. | |
| 299 | + * | |
| 300 | + * @return bool | |
| 301 | + */ | |
| 302 | + public static function supports_drawer( string $provider ): bool { | |
| 303 | + return (bool) ( self::CAPABILITIES[ $provider ]['supports_drawer'] ?? false ); | |
| 304 | + } | |
| 305 | + | |
| 306 | + /** | |
| 307 | + * Per-provider facts the settings screen cannot derive, keyed by provider. | |
| 308 | + * | |
| 309 | + * Projected onto the cloud-print settings response (cf. | |
| 310 | + * Cloud_Print_Relay_Service::public_state()) so the admin app can read the | |
| 311 | + * provider table from the server instead of re-declaring it. Deliberately | |
| 312 | + * narrow: presentation (labels, badges) stays in the client, and facts the | |
| 313 | + * client already renders from its own table are not duplicated here until | |
| 314 | + * something reads them. | |
| 315 | + * | |
| 316 | + * @return array<string, array<string, string>> | |
| 317 | + */ | |
| 318 | + public static function public_capabilities(): array { | |
| 319 | + $capabilities = array(); | |
| 320 | + foreach ( self::valid() as $provider ) { | |
| 321 | + $capabilities[ $provider ] = array( | |
| 322 | + 'template_engines' => self::template_engines( $provider ), | |
| 323 | + ); | |
| 324 | + } | |
| 325 | + | |
| 326 | + return $capabilities; | |
| 138 | 327 | } |
| 139 | 328 | } |