products = $products; $this->settings = $settings; } /** * {@inheritdoc} */ public function register(): void { add_shortcode( 'hostkhata_products', array( $this, 'render' ) ); } /** * Renders the shortcode. * * @since 0.1.0 * * @param array|string $attributes Shortcode attributes. * @return string */ public function render( $attributes = array() ): string { $atts = shortcode_atts( array( 'group' => '', 'type' => ProductType::HOSTING, 'view' => 'cards', 'cycle' => BillingCycle::MONTHLY, 'columns' => '3', 'button' => '', ), is_array( $attributes ) ? $attributes : array(), 'hostkhata_products' ); $type = Sanitizer::enum( $atts['type'], ProductType::all(), ProductType::HOSTING ); $cycle = Sanitizer::enum( $atts['cycle'], BillingCycle::all(), BillingCycle::MONTHLY ); $view = Sanitizer::enum( $atts['view'], array( 'cards', 'table' ), 'cards' ); $columns = max( 1, min( 4, Sanitizer::absint( $atts['columns'] ) ) ); $button = '' !== trim( (string) $atts['button'] ) ? Sanitizer::text( $atts['button'] ) : __( 'Order now', 'hostkhata' ); $items = array(); foreach ( $this->products->public_listing( Sanitizer::key( $atts['group'] ), $type ) as $product ) { $items[] = $this->present( $product, $cycle ); } wp_enqueue_style( 'hostkhata-products', HOSTKHATA_URL . 'assets/public/products.css', array(), HOSTKHATA_VERSION ); return View::render( 'portal/products', array( 'view' => $view, 'columns' => $columns, 'button' => $button, 'products' => $items, 'features' => $this->all_features( $items ), ) ); } /** * Shapes a product for the template. * * @param array $product Product from the repository. * @param string $cycle Preferred billing cycle. * @return array */ private function present( array $product, string $cycle ): array { $primary = $this->primary_price( $product, $cycle ); $others = array(); foreach ( $product['prices'] as $price ) { if ( null !== $primary && $price['id'] === $primary['id'] ) { continue; } $others[] = array( 'label' => PriceType::RECURRING === $price['price_type'] ? BillingCycle::label( $price['billing_cycle'] ) : $this->domain_label( $price ), 'formatted' => $this->settings->format_money( $price['price'] ), ); } return array( 'name' => $product['name'], 'slug' => $product['slug'], 'description' => $product['description'], 'features' => $product['features'], 'popular' => $product['is_popular'], 'price' => null === $primary ? '' : $this->settings->format_money( $primary['price'] ), 'per' => null === $primary ? '' : $this->per_label( $product, $primary ), 'setup' => null !== $primary && $primary['setup_fee'] > 0 ? $this->settings->format_money( $primary['setup_fee'] ) : '', 'others' => $others, 'order_url' => $this->order_url( $product['slug'], null === $primary ? '' : $primary['billing_cycle'] ), ); } /** * The price to show first: the preferred cycle, or the cheapest per month. * * @param array $product Product. * @param string $cycle Preferred cycle. * @return array|null */ private function primary_price( array $product, string $cycle ): ?array { $candidates = array_values( array_filter( $product['prices'], static function ( array $price ) { return in_array( $price['price_type'], array( PriceType::RECURRING, PriceType::REGISTER ), true ); } ) ); if ( array() === $candidates ) { return null; } foreach ( $candidates as $price ) { if ( $price['billing_cycle'] === $cycle ) { return $price; } } usort( $candidates, static function ( array $a, array $b ) { $per_a = $a['price'] / max( 1, $a['period_months'] ); $per_b = $b['price'] / max( 1, $b['period_months'] ); return $per_a <=> $per_b; } ); return $candidates[0]; } /** * Text after the main price. * * @param array $product Product. * @param array $price Price row. * @return string */ private function per_label( array $product, array $price ): string { if ( ProductType::DOMAIN === $product['type'] ) { return $this->domain_period( (int) $price['period_months'] ); } return BillingCycle::per( $price['billing_cycle'] ); } /** * A label for a domain price row, for example "Renew, 1 year". * * @param array $price Price row. * @return string */ private function domain_label( array $price ): string { $names = array( PriceType::REGISTER => __( 'Register', 'hostkhata' ), PriceType::RENEW => __( 'Renew', 'hostkhata' ), PriceType::TRANSFER => __( 'Transfer', 'hostkhata' ), ); return ( $names[ $price['price_type'] ] ?? $price['price_type'] ) . ', ' . $this->domain_period( (int) $price['period_months'] ); } /** * "1 year", "2 years". * * @param int $months Months. * @return string */ private function domain_period( int $months ): string { $years = (int) ( $months / 12 ); /* translators: %d: number of years. */ return sprintf( _n( '%d year', '%d years', $years, 'hostkhata' ), $years ); } /** * Link to the order page for a package, or an empty string when no order page is set. * * @param string $slug Product slug. * @param string $cycle Billing cycle. * @return string */ private function order_url( string $slug, string $cycle ): string { $page_id = (int) $this->settings->get( 'order_page_id' ); $url = $page_id > 0 ? get_permalink( $page_id ) : ''; if ( ! $url ) { return ''; } return add_query_arg( array_filter( array( 'package' => $slug, 'cycle' => $cycle, ) ), $url ); } /** * Every feature that appears in any product, for the comparison table rows. * * @param array> $items Presented products. * @return string[] */ private function all_features( array $items ): array { $features = array(); foreach ( $items as $item ) { $features = array_merge( $features, $item['features'] ); } return array_values( array_unique( $features ) ); } }