diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 16:41:48 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 16:47:14 -0300 |
| commit | 535dbda6b48171f8115b455e54790de94d97d42a (patch) | |
| tree | 5c0410011e61a883bdf063ee612875a637fb1d22 /includes/class-cepis-woo-lookup.php | |
| download | woocommerce-29190494c82855f0f0ce7fb16f831c8f971022c5.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'includes/class-cepis-woo-lookup.php')
| -rw-r--r-- | includes/class-cepis-woo-lookup.php | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/includes/class-cepis-woo-lookup.php b/includes/class-cepis-woo-lookup.php new file mode 100644 index 0000000..47e3f16 --- /dev/null +++ b/includes/class-cepis-woo-lookup.php @@ -0,0 +1,170 @@ +<?php +/** + * Server-side CEP lookups, cached. + * + * @package CepisWooCommerce + */ + +declare(strict_types=1); + +defined( 'ABSPATH' ) || exit; + +/** + * Resolves a CEP with the bundled `Cepis\Client` and remembers the answer in a + * transient. + * + * Only the proxy mode goes through here. In the default direct mode the + * browser talks to cep.is itself and no PHP runs at all. + */ +final class Cepis_Woo_Lookup { + + /** + * Prefix for the transient holding one CEP's answer. + */ + private const CACHE_PREFIX = 'cepis_woo_'; + + /** + * How long a resolved address is kept. + * + * The DNE database behind the API is regenerated fortnightly, so a week is + * half a refresh cycle: an address can be at most one release behind, while + * a CEP a store sees repeatedly still costs one outbound request a week. + */ + private const HIT_TTL = WEEK_IN_SECONDS; + + /** + * How long a "this CEP does not exist" is kept. + * + * Much shorter than a hit, and for a different reason: a CEP absent today + * can appear in the next fortnightly base, and telling a shopper their + * brand-new street does not exist for two weeks is worse than making the + * request again tomorrow. + */ + private const MISS_TTL = DAY_IN_SECONDS; + + /** + * Look a CEP up, from cache when possible. + * + * Never throws: every outcome is described by the returned status and body, + * in exactly the shapes the public API uses, so the browser sees the same + * thing whichever mode the store is in. + * + * @param string $cep Eight digits, already validated by the caller. + * + * @return array{status:int, body:array<string, mixed>, retry_after:?int} + */ + public static function resolve( string $cep ): array { + $cached = get_transient( self::CACHE_PREFIX . $cep ); + + if ( is_array( $cached ) ) { + return isset( $cached['miss'] ) + ? self::not_found() + : array( + 'status' => 200, + 'body' => $cached, + 'retry_after' => null, + ); + } + + try { + $address = self::client()->lookup( $cep ); + } catch ( Cepis\CepNotFoundException ) { + set_transient( self::CACHE_PREFIX . $cep, array( 'miss' => true ), self::MISS_TTL ); + + return self::not_found(); + } catch ( Cepis\InvalidCepException ) { + return array( + 'status' => 400, + 'body' => array( + 'erro' => 'cep_invalido', + 'mensagem' => 'CEP deve ter 8 dígitos', + ), + 'retry_after' => null, + ); + } catch ( Cepis\RateLimitedException $e ) { + return array( + 'status' => 429, + 'body' => array( + 'erro' => 'limite', + 'mensagem' => 'Muitas consultas em pouco tempo', + ), + 'retry_after' => null === $e->retryAfter ? null : (int) $e->retryAfter, + ); + } catch ( Cepis\CepisException ) { + return array( + 'status' => 502, + 'body' => array( + 'erro' => 'interno', + 'mensagem' => 'falha interna', + ), + 'retry_after' => null, + ); + } + + $body = $address->raw; + set_transient( self::CACHE_PREFIX . $cep, $body, self::HIT_TTL ); + + return array( + 'status' => 200, + 'body' => $body, + 'retry_after' => null, + ); + } + + /** + * Drop every cached answer. + * + * Used by the settings screen when the API root changes, since answers from + * one instance say nothing about another. + */ + public static function flush(): void { + global $wpdb; + + $like = $wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'; + + $names = $wpdb->get_col( + $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) + ); + + foreach ( (array) $names as $name ) { + delete_transient( substr( (string) $name, strlen( '_transient_' ) ) ); + } + } + + /** + * The client, built once per request. + */ + private static function client(): Cepis\Client { + static $client = null; + + if ( null === $client ) { + $client = new Cepis\Client( + baseUrl: Cepis_Woo_Options::base_url(), + timeout: 5.0, + maxAttempts: 2, + totalTimeout: 8.0, + userAgent: 'cepis-woocommerce/' . CEPIS_WOO_VERSION . ' (+https://cep.is)', + transport: new Cepis_Woo_Transport(), + ); + } + + return $client; + } + + /** + * The API's own 404 body, reproduced so the browser cannot tell the two + * modes apart. + * + * @return array{status:int, body:array<string, mixed>, retry_after:?int} + */ + private static function not_found(): array { + return array( + 'status' => 404, + 'body' => array( + 'erro' => 'cep_nao_encontrado', + 'mensagem' => 'CEP não existe na base do DNE', + ), + 'retry_after' => null, + ); + } +} |