Hàm import cơ sở dữ liệu vào MySQL

Nếu bạn biết thông tin kết nối đến cơ sở dữ liệu nhưng không có cách nào để import database thì bạn có thể sử dụng hàm PHP để nhập cơ sở dữ liệu vào MySQL Server.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>MySQL Tools by Lai Dinh Cuong</title>
</head>
<body>
<?php
/**
 * Imports a large .sql file into a MySQL database.
 *
 * @param string $filePath Path to the .sql file.
 * @param array $dbConfig Database configuration array with keys: host, username, password, database.
 *
 * @return bool True on success, false on failure.
 */
function importLargeSqlFileToMySQL( $filePath, $dbConfig ) {
    $db_host = $dbConfig['host'] ?? '';

    if ( empty( $db_host ) ) {
        $db_host = 'localhost';
    }

    // Create a new PDO instance
    $dsn = "mysql:host={$db_host};dbname={$dbConfig['database']};charset=utf8";

    try {
        $pdo = new PDO( $dsn, $dbConfig['username'], $dbConfig['password'], [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ] );
    } catch ( PDOException $e ) {
        die( 'Connection failed: ' . $e->getMessage() );
    }

    // Ensure the file exists
    if ( ! file_exists( $filePath ) ) {
        die( 'File not found: $filePath' );
    }

    // Open the SQL file
    $handle = fopen( $filePath, 'r' );

    if ( $handle === false ) {
        die( 'Failed to open file: $filePath' );
    }

    // Read and execute the file line by line
    $query = '';

    try {
        $pdo->beginTransaction();

        while ( ( $line = fgets( $handle ) ) !== false ) {
            $trimmedLine = trim( $line );

            // Skip comments and empty lines
            if ( empty( $trimmedLine ) || strpos( $trimmedLine, '--' ) === 0 || strpos( $trimmedLine, '/*' ) === 0 || strpos( $trimmedLine, '//' ) === 0 ) {
                continue;
            }

            // Append line to current query
            $query .= $line;

            // If query ends with a semicolon, execute it
            if ( substr( trim( $query ), - 1 ) === ';' ) {
                // Check if the query is a CREATE TABLE statement
                if ( preg_match( '/CREATE TABLE `?(\w+)`?/', $query, $matches ) ) {
                    $tableName = $matches[1];
                    // Drop the table if it exists
                    $pdo->exec( "DROP TABLE IF EXISTS `$tableName`" );
                }

                try {
                    $pdo->exec( $query );
                } catch ( PDOException $e ) {
                    // Log error and continue
                    echo 'Error executing query: ' . $e->getMessage() . "\n";
                }

                $query = ''; // Reset query
            }
        }

        $pdo->commit();

        fclose( $handle );

        return true;
    } catch ( PDOException $e ) {
        if ( $pdo->inTransaction() ) {
            $pdo->rollBack();
        }

        // Query to get the size of the database
        $sql = "SELECT table_schema AS `Database`, 
                   SUM(data_length + index_length) AS `Size` 
            FROM information_schema.tables 
            WHERE table_schema = :database 
            GROUP BY table_schema";

        $size = false;

        try {
            $stmt = $pdo->prepare( $sql );
            $stmt->bindParam( ':database', $dbConfig['database'], PDO::PARAM_STR );
            $stmt->execute();
            $result = $stmt->fetch();
            $size   = $result ? (float) $result['Size'] : 0;
        } catch ( PDOException $e ) {
            die( 'Failed to get database size: ' . $e->getMessage() );
        }

        fclose( $handle );

        if ( $size ) {
            return true;
        } else {
            die( 'Import failed: ' . $e->getMessage() );
        }
    }
}

// Example usage
$dbConfig = [
    'host'     => $_GET['db_host'] ?? '',
    'username' => $_GET['db_user'] ?? '',
    'password' => $_GET['db_password'] ?? '',
    'database' => $_GET['db_name'] ?? '',
];

$filePath = $_GET['file'] ?? '';

if ( empty( $filePath ) || ! file_exists( $filePath ) ) {
    $filePath = 'database.sql';
}

if ( importLargeSqlFileToMySQL( $filePath, $dbConfig ) ) {
    echo 'Import successful!';
} else {
    echo 'Import failed.';
}
?>
</body>
</html>

 

Thêm tham số tùy chỉnh cho phân trang

Khi sử dụng phân trang tùy chỉnh cho một danh sách nào đó hiển thị trong trang single hoặc trang archive. Chúng ta sẽ không sử dụng tham số paged thông thường, thay vào đó mình sẽ truyền vào tham số pagi để sử dụng.

add_filter( 'hocwp_theme_paginate_links_args', function ( $args ) {
    if ( is_author() ) {
        $agent = new LDC_Agent( get_queried_object_id() );

        if ( $agent->is_valid() ) {
            $base = $args['base'] ?? '';

            if ( ! empty( $base ) ) {
                $base = str_replace( 'page/%#%/', '', $base );
                $base = remove_query_arg( 'pagi', $base );

                if ( str_contains( $base, '?' ) ) {
                    $base .= '&';
                } else {
                    $base .= '?';
                }

                $base .= 'pagi=%#%';

                $args['base'] = $base;
            }
        }
    }

    return $args;
} );

add_filter( 'hocwp_theme_current_paged', function ( $paged ) {
    if ( empty( $paged ) ) {
        $paged = absint( $_GET['pagi'] ?? '' );
    }

    return $paged;
} );

 

Hàm lấy user theo số lượng bài viết sử dụng $wpdb

Truy vấn người dùng theo số lượng bài viết, sắp xếp từ cao đến thấp, có thể tìm kiếm theo display_name hoặc user_login.

public function get_users_by_post_count( $post_type = 'post', $number = 5, $search = '', $role = 'agent' ) {
    global $wpdb;

    $sql = "SELECT {$wpdb->users}.ID, p.post_count FROM {$wpdb->users}";
    $sql .= " LEFT JOIN ( SELECT post_author, COUNT(*) AS post_count FROM {$wpdb->posts} WHERE post_type = %s GROUP BY post_author ) p ON {$wpdb->users}.id = p.post_author";

    if ( ! empty( $search ) ) {
        $sql .= " WHERE {$wpdb->users}.display_name LIKE '%" . $search . "%' OR {$wpdb->users}.user_login LIKE '%" . $search . "%'";
    }

    $sql .= " ORDER BY p.post_count DESC";

    $users = $wpdb->get_results( $wpdb->prepare( $sql, $post_type ) );

    if ( ! empty( $role ) ) {
        $lists = array();

        foreach ( $users as $obj ) {
            $user = get_user_by( 'id', $obj->ID );

            if ( $user instanceof WP_User ) {
                if ( in_array( $role, $user->roles ) ) {
                    $lists[] = $user;
                }
            }
        }

        $users = $lists;
    }

    if ( HT()->is_positive_number( $number ) ) {
        $users = array_slice( $users, 0, $number );
    }

    return $users;
}

 

Làm slide hình ảnh chạy sang trái phải đơn giản

Trong ví dụ này, mình có hơn 10 hình ảnh hiển thị theo chiều ngang, trong màn hình chỉ có thể hiển thị 10 hình ảnh mà thôi, hoặc thậm chí hiển thị ít hơn. Những hình ảnh còn lại sẽ nằm bên ngoài container, để xem được các hình ẩn này mình sẽ dùng 2 nút previous và next để chạy theo dạng slide.

Bên dưới là đoạn mã PHP và HTML ví dụ:

<?php
defined( 'ABSPATH' ) || exit;

if ( is_active_sidebar( 'home_banners' ) ) {
    ?>
    <div class="section home-banners">
        <div class="container">
            <div class="slider-box">
                <div class="banners d-flex no-wrap overflow-auto scrollbar-hide scroll-smooth">
                    <?php dynamic_sidebar( 'home_banners' ); ?>
                </div>
                <div class="absolute top-[50%] left-1 translate-y-[-50%] prev controls">
                    <button class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall !bg-white mui-style-1j7qk7u"
                            tabindex="0" type="button">
                        <svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium mui-style-19xldyr" focusable="false"
                             aria-hidden="true" viewBox="0 0 24 24" data-testid="ArrowBackIosNewIcon">
                            <path d="M17.77 3.77 16 2 6 12l10 10 1.77-1.77L9.54 12z"></path>
                        </svg>
                        <span class="MuiTouchRipple-root mui-style-w0pj6f"></span></button>
                </div>
                <div class="absolute top-[50%] right-1 translate-y-[-50%] next controls">
                    <button class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall !bg-white mui-style-1j7qk7u"
                            tabindex="0" type="button">
                        <svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium mui-style-19xldyr" focusable="false"
                             aria-hidden="true" viewBox="0 0 24 24" data-testid="ArrowForwardIosIcon">
                            <path d="M6.23 20.23 8 22l10-10L8 2 6.23 3.77 14.46 12z"></path>
                        </svg>
                        <span class="MuiTouchRipple-root mui-style-w0pj6f"></span></button>
                </div>
            </div>
        </div>
    </div>
    <?php
}

Đoạn code trên mình sẽ hiện thị các widget chứa hình ảnh vào thẻ div có class là banners.

Ảnh demo carousel đơn giản bằng Javascript
Ảnh demo carousel đơn giản bằng Javascript

Bên dưới là đoạn Javascript để xử lý các sự kiện click nút chạy sang phải và trái.

window.HOCWP_THEME = window.HOCWP_THEME || {};

jQuery(document).ready(function ($) {
    const BODY = $("body");

    HOCWP_THEME.FRONTEND = {
        init: function () {
            this.homeBanners();
        },
        homeBanners: function () {
            let offset = null,
                offsetLeft = null,
                offsetRight = null;

            function update_carousel(box, banners, margin_left = 0, end = false) {
                if (0 !== margin_left) {
                    margin_left += "px";
                }

                banners.animate({marginLeft: margin_left}, "slow");

                if (end) {
                    box.addClass("end");
                    box.removeClass("begin");
                } else {
                    box.addClass("begin");
                    box.removeClass("end");
                }

                banners.removeClass("change-margin");
                banners.attr("data-change-margin", 0);
            }

            function banners_carousel(element, init = false) {
                let box = element.closest(".slider-box"),
                    banners = box.find(".banners"),
                    gap = parseInt(banners.css("gap")),
                    widgets = banners.find(".widget"),
                    widget = widgets.first(),
                    step = widget.width(),
                    width = (step * widgets.length) + (gap * widgets.length) - gap,
                    hidden = width - box.width(),
                    direction,
                    margin;

                if (!offset) {
                    // Current offset left
                    offset = banners.offset().left;
                    // Keep offset left
                    offsetLeft = offset;
                    offsetRight = offsetLeft + box.width();
                }

                if (init) {
                    box.attr("data-offset-left", offsetLeft);
                    box.attr("data-current-offset", offset);
                    box.attr("data-offset-right", offsetRight);
                    box.attr("width", width);
                    box.attr("data-outside", hidden);
                    update_carousel(box, banners);

                    return {
                        step: offset,
                        direction: "default"
                    };
                }

                if (offset >= offsetLeft) {
                    box.addClass("begin");
                }

                if (element.hasClass("prev")) {
                    if (!box.hasClass("begin")) {
                        offset += step;
                        margin = "+";
                    }

                    direction = "prev";
                } else {
                    if (!box.hasClass("end")) {
                        offset -= step;
                        margin = "-";
                    }

                    direction = "next";
                }

                if (margin) {
                    let marginLeft = parseInt(banners.css("margin-left"));

                    if (banners.hasClass("change-margin")) {
                        step -= parseInt(banners.attr("data-change-margin"));
                        banners.removeClass("change-margin");
                        banners.attr("data-change-margin", 0);
                    }

                    banners.animate({marginLeft: margin + "=" + step + "px"}, {
                        duration: 500,
                        complete: function () {
                            marginLeft = parseInt(banners.css("margin-left"));

                            box.attr("data-margin", marginLeft);

                            if (0 === marginLeft || -0 === marginLeft || marginLeft === (gap - 2)) {
                                update_carousel(box, banners);
                            } else if (0 > marginLeft && (hidden + marginLeft) <= 0) {
                                // Fix last item has no gap
                                banners.animate({marginLeft: "+=" + gap + "px"}, "slow");
                                banners.attr("data-change-margin", gap);
                                banners.addClass("change-margin");

                                box.addClass("end");
                                box.removeClass("begin");
                            } else {
                                box.removeClass("begin end");
                            }

                            element.removeClass("waiting");

                            if ("prev" === direction && (Math.abs(marginLeft) < step || marginLeft > 0)) {
                                update_carousel(box, banners);
                            } else if ("next" === direction) {
                                if (Math.abs(marginLeft) > hidden) {
                                    update_carousel(box, banners);
                                } else if (Math.abs(Math.abs(marginLeft) - hidden) < step) {
                                    update_carousel(box, banners, -hidden, true);
                                }
                            }
                        }
                    });
                }

                box.attr("data-current-offset", offset);

                setTimeout(function () {
                    element.removeClass("waiting");
                }, 1000);

                return {
                    step: offset,
                    direction: direction
                };
            }

            banners_carousel(BODY.find(".home-banners .controls"), true);

            BODY.on("click", ".home-banners .controls", function (e) {
                e.preventDefault();
                let that = this,
                    element = $(that);

                if (!element.hasClass("waiting")) {
                    element.addClass("waiting");
                    banners_carousel(element);
                }
            });
        }
    };

    HOCWP_THEME.FRONTEND.init();
});

Với đoạn mã JS bên trên chúng ta chỉ có thể áp dụng cho các widget có cùng chiều rộng. Vì công thức tính chiều rộng lúc này sẽ lấy chiều rộng của 1 widget đầu tiên và nhân cho số lượng widget sẵn có. Để đoạn code chạy đúng hơn, mình cập nhật lại một chút để tính chiều rộng của các widget:

let box = element.closest(".slider-box"),
    banners = box.find(".banners"),
    gap = parseInt(banners.css("gap")),
    widgets = banners.find(".widget"),
    widget = widgets.first(),
    step = widget.width(),
    width = 0;

widgets.each(function () {
    let widgetWidth = $(this).width();

    if (step > widgetWidth) {
        step = widgetWidth;
    }

    width += widgetWidth + gap;
});

width -= gap;

let hidden = width - box.width(),
    direction,
    margin;

Cài đặt Object Cache với Redis trên plugin Litespeed Cache

Nếu bạn đang sử dụng plugin Litespeed Cache, phần Object Cache không kích hoạt hoặc kích hoạt sai thì sẽ thấy thông báo: LSCache caching functions on this page are currently unavailable!

Cài đặt Object Cache với Redis trên plugin Litespeed Cache
Cài đặt Object Cache với Redis trên plugin Litespeed Cache

Trạng thái kiểm tra kết nối của Object Cache sử dụng Memcached hoặc Redis là Thất bại. Trong bài viết này mình sẽ hướng dẫn cho bạn cách kích hoạt Object Cache bằng Redis trên plugin Litespeed Cache cho gói cPanel shared hosting của AZDIGI.

Cài đặt Object Cache với Redis trên plugin Litespeed Cache

Đầu tiên, bạn đăng nhập vào cPanel và tìm tới mục Advanced, nhấn vào plugin AZ Redis.

Kích hoạt AZ Redis trên cPanel
Kích hoạt AZ Redis trên cPanel

Tiếp đến, bạn nhấn nút Enable nếu chưa kích hoạt trước đó. Đợi một vài giây bạn sẽ thấy 1 đường dẫn Socket Path hiển thị lên, bạn sao chép đường dẫn này.

Tiếp theo, bạn cài đặt và kích hoạt plugin Litespeed Cache, sau đó vào menu Litespeed Cache → Cache → Object.

Cuối cùng, bạn điền đường dẫn socket vừa sao chép vào ô Host. Chọn Bật Object Cache, chọn phương pháp là Redis, nhập port là 0 như hình đầu tiên của bài viết. Chúc bạn thành công.

Nếu bạn có bất kỳ thắc mắc nào cần giải đáp, hãy gửi liên hệ ngay cho HocWP Team để được tư vấn nhé. Bên cạnh đó, bạn cũng có thể đăng ký theo dõi kênh YouTube Học WordPress để cập nhật kiến thức về quản lý và phát triển website nhé.

Khởi tạo slick, sự kiện on init và cập nhật tham số sau khi slide đã khởi tạo

Slick là plugin jQuery cho phép bạn tạo ra slide carousel một cách nhanh chóng. Bên dưới là ví dụ cách khởi tạo slick, gọi sự kiện init và thiết lập lại tham số tùy chọn sau khi slide đã khởi tạo.

(function () {
    let elements = $(".flexslider .slides"),
        settings = {
            autoplay: true,
            dots: true,
            fade: true,
            adaptiveHeight: false
        };

    if (body.hasClass("single")) {
        settings.autoplay = false;
    }

    elements.on("init", function (slick) {
        console.log("Slide initialized!");
    });

    elements.slick(settings);

    setTimeout(function () {
        // Update slide option after 1 second
        elements.slick("slickSetOption", "adaptiveHeight", true, true);
    }, 1000)
})();

 

Kết nối internet Việt Nam đi quốc tế bị ảnh hưởng do đứt cáp quang biển ở Biển Đỏ

Trong tuần qua, ít nhất 4 tuyến cáp quang biển chịu trách nhiệm truyền tải 97% lưu lượng internet đã bị hỏng tại Biển Đỏ. Các nhà cung cấp viễn thông đang báo cáo nhưng sự bất ổn tại Yemen đã làm ngăn cản các nhà khai thác sửa chữa chúng.

Đứt cáp quang biển ở Biển Đỏ
Đứt cáp quang biển ở Biển Đỏ

HGC Communications, một nhà điều hành mạng viễn thông tại Hồng Kong đã báo cáo về sự cố của 4 tuyến cáp quang biển, bao gồm: SEACOM, TGN, Africa Asia Europe-One và Europe India Gateway. HGC cho biết, sự cố đã làm ảnh hưởng đến 25% lưu lượng internet của họ, hiện đang được chuyển hướng qua Trung Quốc lục địa về phía Biển Đông thông qua Hoa Kỳ.

Do vậy, trong tuần qua các bạn sử dụng dịch vụ internet quốc tế hoặc sử dụng WordPress có các đường link tải dữ liệu từ server của WordPress thì sẽ bị lỗi timeout. Hiện chưa có lịch sửa chữa các sự cố này.

Xóa các scripts của WooCommerce trên các trang khác nhau

Nếu bạn muốn tăng tốc trang web của mình khi sử dụng WooCommerce, bạn có thể sử dụng hàm điều kiện để loại bỏ các style và script được tạo ra bởi plugin WooCommerce.

/**
 * Manage WooCommerce styles and scripts.
 */
function grd_woocommerce_script_cleaner() {
    
    // Remove the generator tag
    remove_action( 'wp_head', array( $GLOBALS['woocommerce'], 'generator' ) );

    // Unless we're in the store, remove all the cruft!
    if ( ! is_woocommerce() && ! is_cart() && ! is_checkout() ) {
        wp_dequeue_style( 'woocommerce_frontend_styles' );
        wp_dequeue_style( 'woocommerce-general');
        wp_dequeue_style( 'woocommerce-layout' );
        wp_dequeue_style( 'woocommerce-smallscreen' );
        wp_dequeue_style( 'woocommerce_fancybox_styles' );
        wp_dequeue_style( 'woocommerce_chosen_styles' );
        wp_dequeue_style( 'woocommerce_prettyPhoto_css' );
        wp_dequeue_script( 'selectWoo' );
        wp_deregister_script( 'selectWoo' );
        wp_dequeue_script( 'wc-add-payment-method' );
        wp_dequeue_script( 'wc-lost-password' );
        wp_dequeue_script( 'wc_price_slider' );
        wp_dequeue_script( 'wc-single-product' );
        wp_dequeue_script( 'wc-add-to-cart' );
        wp_dequeue_script( 'wc-cart-fragments' );
        wp_dequeue_script( 'wc-credit-card-form' );
        wp_dequeue_script( 'wc-checkout' );
        wp_dequeue_script( 'wc-add-to-cart-variation' );
        wp_dequeue_script( 'wc-single-product' );
        wp_dequeue_script( 'wc-cart' );
        wp_dequeue_script( 'wc-chosen' );
        wp_dequeue_script( 'woocommerce' );
        wp_dequeue_script( 'prettyPhoto' );
        wp_dequeue_script( 'prettyPhoto-init' );
        wp_dequeue_script( 'jquery-blockui' );
        wp_dequeue_script( 'jquery-placeholder' );
        wp_dequeue_script( 'jquery-payment' );
        wp_dequeue_script( 'fancybox' );
        wp_dequeue_script( 'jqueryui' );
    }
}
add_action( 'wp_enqueue_scripts', 'grd_woocommerce_script_cleaner', 999 );

Đối với các bạn sử dụng WordPressWooCommerce phiên bản mới, bạn có thể sử dụng hook bên dưới:

add_action('template_redirect', 'conditionally_disable_wc_assets');
/**
    * Remove WC stuff on non WC pages.
    */
function conditionally_disable_wc_assets()
{
    if (IF_CONDTION_MET) {
        // remove WC generator tag
        remove_filter('get_the_generator_html', 'wc_generator_tag', 10, 2);
        remove_filter('get_the_generator_xhtml', 'wc_generator_tag', 10, 2);
        // unload WC scripts
        remove_action('wp_enqueue_scripts', [WC_Frontend_Scripts::class, 'load_scripts']);
        remove_action('wp_print_scripts', [WC_Frontend_Scripts::class, 'localize_printed_scripts'], 5);
        remove_action('wp_print_footer_scripts', [WC_Frontend_Scripts::class, 'localize_printed_scripts'], 5);
        // remove "Show the gallery if JS is disabled"
        remove_action('wp_head', 'wc_gallery_noscript');
    }
}

Các bạn thay IF_CONDTION_MET thành câu điều kiện cần thiết để loại bỏ scripts của WooCommerce nhé.

Dữ liệu đơn vị hành chính Việt Nam đến cấp quận huyện, phường xã

Chia sẻ dữ liệu địa giới hành chính của Việt Nam (đơn vị hành chính Việt Nam), chi tiết từ cấp tỉnh thành phố đến cấp quận huyện, phường xã. Nếu bạn nào đang làm web mà cần đụng tới dữ liệu địa giới hành chính thì có thể tải về để dùng, bản cập nhật được tải ngày 15/02/2024.

Địa giới hành chính Việt Nam đến cấp xã
Địa giới hành chính Việt Nam đến cấp xã

Link tải: https://danhmuchanhchinh.gso.gov.vn/

Sau khi truy cập vào trang web, các bạn có thể xuất tập tin dưới dạng file Excel, sau đó tải lên Google Drive hoặc dùng bất kỳ công cụ nào khác để chuyển đổi sang đuôi cần sử dụng. Mình hay chuyển đổi file .xlsx thành file .csv để sử dụng.

Thêm column và sortable column vào bảng danh sách bài viết

Đoạn code ví dụ cách thêm column và sortable column vào bảng quản lý danh sách bài viết trong admin.

add_filter( 'manage_posts_columns', 'hocwp_theme_custom_post_columns_filter' );
function hocwp_theme_custom_post_columns_filter( $columns ) {
    $date = $columns['date'];
    unset( $columns['date'] );
    $columns['type'] = __( 'Type', 'rentinsingapore' );

    $columns['date'] = $date;

    return $columns;
}

add_action( 'manage_posts_custom_column', 'hocwp_theme_custom_post_column_action', 10, 2 );
function hocwp_theme_custom_post_column_action( $column, $post_id ) {
    switch ( $column ) {
        // display a thumbnail photo
        case 'type' :
            $type = get_post_meta( $post_id, 'type', true );

            if ( empty( $type ) ) {
                $type = 'room';
            }

            echo $type;
            break;
    }
}

add_filter( 'manage_edit-post_sortable_columns', 'hocwp_theme_custom_post_sortable_columns_filter' );
function hocwp_theme_custom_post_sortable_columns_filter( $columns ) {
    $columns['type'] = 'type';

    return $columns;
}

add_action( 'pre_get_posts', 'hocwp_theme_custom_sort_post_column' );
function hocwp_theme_custom_sort_post_column( $query ) {
    if ( ! is_admin() ) {
        return;
    }

    $orderby = $query->get( 'orderby' );

    if ( 'type' == $orderby ) {
        $query->set( 'meta_key', 'type' );
        $query->set( 'orderby', 'meta_value' );
    }
}

Thêm đoạn code bên trên vào tập tin functions.php của giao diện hoặc plugin.

Thêm column và sortable column vào bảng danh sách bài viết

Trong ví dụ bên trên, cột Type sẽ được thêm vào bảng danh sách bài viết. Khi nhấn chuột vào cột Type sẽ sắp xếp danh sách theo giá trị của cột.

Ở đây, tên cột sắp xếp đặt trùng với tên meta key lưu trong cơ sở dữ liệu nên giá trị type của tham số orderby và trường thiết lập meta_key của query là giống nhau.

Ví dụ này chỉ áp dụng cho post_typepost. Đối với page và các custom post type khác thì các hook sẽ thay đổi một chút thì mới có thể hoạt động.

Nếu chỉ áp dụng cho post type là post (bài viết) thì sử dụng các hook sau:

//add_filter( 'manage_posts_columns', array( $this, 'manage_posts_columns_filter' ), 99, 2 );
//add_action( 'manage_posts_custom_column', array( $this, 'manage_posts_custom_column_action' ), 99, 2 );
//add_filter( 'manage_edit-post_sortable_columns', 'hocwp_theme_custom_post_sortable_columns_filter' );

Nếu áp dụng thêm column cho tất cả các post type, bao gồm: post, page và custom post type thì áp dụng như code bên dưới:

<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}

class Content_To_Speech_Post_Columns {
    public function __construct() {
        $post_type = Content_To_Speech_Global()->get_option( 'post_type' );

        if ( is_array( $post_type ) ) {
            foreach ( $post_type as $pt ) {
                add_filter( 'manage_' . $pt . '_posts_columns', array( $this, 'manage_posts_columns_filter' ), 99, 2 );

                add_action( 'manage_' . $pt . '_posts_custom_column', array(
                    $this,
                    'manage_posts_custom_column_action'
                ), 99, 2 );

                add_filter( 'manage_edit-' . $pt . '_sortable_columns', array(
                    $this,
                    'manage_posts_sortable_columns_filter'
                ) );
            }
        }

        add_action( 'pre_get_posts', array( $this, 'pre_get_posts_action' ) );
    }

    public function pre_get_posts_action( $query ) {
        if ( is_admin() && $query instanceof WP_Query ) {
            global $pagenow;

            if ( 'edit.php' == $pagenow ) {
                $by    = $_GET['orderby'] ?? '';
                $order = $_GET['order'] ?? 'DESC';

                if ( 'audio_file' == $by ) {
                    $query->set( 'meta_key', CTS_CONST()::PM_AUDIO_FILE );
                    $query->set( 'orderby', 'meta_value' );
                    $query->set( 'order', $order );
                }
            }
        }
    }

    public function manage_posts_sortable_columns_filter( $columns ) {
        $columns['audio_file'] = 'audio_file';

        return $columns;
    }

    public function manage_posts_columns_filter( $posts_columns ) {
        $date = $posts_columns['date'] ?? '';
        unset( $posts_columns['date'] );
        $posts_columns['audio_file'] = __( 'Audio File', CTS_CONST()::TEXT_DOMAIN );

        if ( ! empty( $date ) ) {
            $posts_columns['date'] = $date;
        }

        return $posts_columns;
    }

    public function manage_posts_custom_column_action( $column_name, $post_id ) {
        if ( 'audio_file' == $column_name ) {
            $file = get_post_meta( $post_id, CTS_CONST()::PM_AUDIO_FILE, true );

            if ( empty( $file ) ) {
                ?>
                <button type="button"
                        class="button button-primary" data-ajax-button="1" data-do-action="generate_audio_file"
                        data-id="<?php echo esc_attr( $post_id ); ?>"><?php _e( 'Create Audio File', CTS_CONST()::TEXT_DOMAIN ); ?></button>
                <?php
            } else {
                Content_To_Speech_Global()->audio_html( $post_id );
                ?>
                <button type="button" class="button button-secondary play-audio"
                        data-target="Audio<?php echo esc_attr( $post_id ); ?>"><span
                            class="dashicons dashicons-controls-play"></span></button>
                <button type="button"
                        class="button button-primary" data-ajax-button="1" data-do-action="generate_audio_file"
                        data-id="<?php echo esc_attr( $post_id ); ?>"><?php _e( 'Update Audio File', CTS_CONST()::TEXT_DOMAIN ); ?></button>
                <?php
            }
        }
    }
}

new Content_To_Speech_Post_Columns();

 

Class chuyển nội dung bài viết thành giọng đọc file audio mp3

Class bên dưới sẽ mô tả cách chuyển nội dung bài viết thành giọng đọc (Text to Speech, Content to Speech).

<?php
defined( 'ABSPATH' ) || exit;

class CTS_Text_To_Speech {
    public $api_key;
    public $voice_id;
    public $text;
    public $error;
    public $response;
    private $voice;

    public function __construct( $api_key, $voice_id, $text ) {
        $this->set_api_key( $api_key );
        $this->set_voice_id( $voice_id );
        $this->set_text( $text );
        $this->error = new WP_Error();
    }

    public function get_voice() {
        $voices = $this->get_voice_ids();
        $this->sanitize_voices_response( $voices );

        if ( is_array( $voices ) ) {
            foreach ( $voices as $voice ) {
                if ( is_object( $voice ) && isset( $voice->voice_id ) && $this->voice_id == $voice->voice_id ) {
                    $this->voice = $voice;
                    break;
                }
            }
        }

        return $this->voice;
    }

    public function get_voice_ids() {
        $tr_name = 'elevenlabs_voice_ids';

        if ( false === ( $res = get_transient( $tr_name ) ) ) {
            $url = 'https://api.elevenlabs.io/v1/voices';
            $res = wp_remote_retrieve_body( wp_remote_get( $url ) );

            if ( ! empty( $res ) ) {
                $res = json_decode( $res );
                $this->sanitize_voices_response( $res );

                set_transient( $tr_name, $res );
            }
        }

        return $res;
    }

    public function set_api_key( $api_key ) {
        $this->api_key = $api_key;
    }

    private function sanitize_voices_response( &$res ) {
        if ( is_object( $res ) && isset( $res->voices ) ) {
            $res = $res->voices;
        }
    }

    public function set_voice_id( $voice_id ) {
        if ( empty( $voice_id ) || 'rand' == $voice_id || 'random' == $voice_id ) {
            $voices = $this->get_voice_ids();
            $this->sanitize_voices_response( $voices );

            if ( is_array( $voices ) ) {
                shuffle( $voices );

                foreach ( $voices as $obj ) {
                    if ( is_object( $obj ) && isset( $obj->voice_id ) ) {
                        $voice_id = $obj->voice_id;
                        break;
                    }
                }
            }
        }

        $this->voice_id = $voice_id;
    }

    public function set_text( $text ) {
        $this->text = $text;
    }

    public function get_base_url() {
        return 'https://api.elevenlabs.io/v1/text-to-speech/' . $this->voice_id;
    }

    public function calculate_timeout() {
        $count = str_word_count( strip_tags( $this->text ) );
        $count = ceil( $count / 150 );

        if ( $count < MINUTE_IN_SECONDS ) {
            $count = MINUTE_IN_SECONDS;
        }

        return $count;
    }

    public function generate( $type = 'remote' ) {
        if ( 'curl' == $type ) {
            $this->response = $this->curl();
        } else {
            $this->response = $this->remote();
        }

        if ( is_string( $this->response ) && str_contains( $this->response, 'detail' ) ) {
            $res = json_decode( $this->response );

            if ( is_object( $res ) && isset( $res->detail ) ) {
                $this->response = '';

                if ( is_object( $res->detail ) && isset( $res->detail->status ) ) {
                    $this->error->add( $res->detail->status, $res->detail->message );
                } else {
                    $this->error->add( 'response_detail', $res->detail );
                }
            }
        }

        return $this->response;
    }

    private function build_body() {
        return wp_json_encode( array(
            'text' => $this->text
        ) );
    }

    private function build_header() {
        return array(
            'Content-Type' => 'application/json',
            'xi-api-key'   => $this->api_key
        );
    }

    private function remote() {
        $args = array(
            'timeout' => $this->calculate_timeout(),
            'headers' => $this->build_header(),
            'body'    => $this->build_body()
        );

        $res = wp_remote_post( $this->get_base_url(), $args );

        if ( is_wp_error( $res ) ) {
            $this->error->merge_from( $res );
        }

        return wp_remote_retrieve_body( $res );
    }

    private function curl() {
        $curl = curl_init();

        $headers = $this->build_header();
        $header  = array();

        foreach ( $headers as $key => $value ) {
            $header[] = $key . ': ' . $value;
        }

        curl_setopt_array( $curl, [
            CURLOPT_URL            => $this->get_base_url(),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING       => '',
            CURLOPT_MAXREDIRS      => 10,
            CURLOPT_TIMEOUT        => $this->calculate_timeout(),
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST  => 'POST',
            CURLOPT_POSTFIELDS     => $this->build_body(),
            CURLOPT_HTTPHEADER     => $header
        ] );

        $response = curl_exec( $curl );
        $err      = curl_error( $curl );

        curl_close( $curl );

        if ( $err ) {
            $this->error->add( 'curl_error', $err );
        }

        return $response;
    }
}

Đoạn code ví dụ bên trên sử dụng API của ElevenLabs. Gói miễn phí được sử dụng 10.000 ký tự mỗi tháng.

Thêm column và sortable column vào bảng thành viên

Nếu bạn muốn thêm một cột bất kỳ vào bảng danh sách người dùng, bạn có thể tham khảo đoạn code bên dưới.

function hpxf_manage_users_sortable_columns_filter( $column ) {
    $column['date']        = 'date';
    $column['collections'] = 'collections';
    $column['posts']       = 'posts';
    $column['forum_posts'] = 'forum_posts';
    $column['media_files'] = 'media_files';
    $column['description'] = 'description';

    return $column;
}

add_filter( 'manage_users_sortable_columns', 'hpxf_manage_users_sortable_columns_filter' );

function hpxf_manage_users_columns_filter( $column ) {
    $date = $column['date'] ?? '';

    if ( ! isset( $column['posts'] ) ) {
        $column['posts'] = __( 'Posts', 'pixelify' );
    }

    $lists = array();

    foreach ( $column as $key => $text ) {
        $lists[ $key ] = $text;

        if ( 'posts' == $key ) {
            $lists['forum_posts'] = __( 'Forum Posts', 'pixelify' );
        }
    }

    $column = $lists;
    $column['description'] = __( 'Description', 'pixelify' );
    $column['collections'] = __( 'Collections', 'pixelify' );
    $column['media_files'] = __( 'Media Files', 'pixelify' );

    if ( ! empty( $date ) ) {
        $column['date'] = $date;
    } else {
        $column['date'] = __( 'Date', 'pixelify' );
    }

    return $column;
}

add_filter( 'manage_users_columns', 'hpxf_manage_users_columns_filter' );

function hpxf_manage_users_custom_column_action( $val, $column_name, $user_id ) {
    global $wpdb;

    switch ( $column_name ) {
        case 'date' :
            return get_the_author_meta( 'registered', $user_id );
        case 'collections':
            return count_user_posts( $user_id, 'collection' );
        case 'description':
            return get_the_author_meta( 'description', $user_id );
        case 'forum_posts':
            return count_user_posts( $user_id, 'font_forum' );
        case 'media_files':
            $count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'attachment' AND post_author = $user_id" );

            return sprintf( '<a href="%s" target="_blank">%s</a>', esc_url( admin_url( 'upload.php?author=' . $user_id ) ), $count );
    }

    return $val;
}

add_filter( 'manage_users_custom_column', 'hpxf_manage_users_custom_column_action', 10, 3 );

function pxf_pre_user_query_action( $query ) {
    if ( $query instanceof WP_User_Query ) {
        global $wpdb, $current_screen;

        // Only filter in the admin
        if ( ! is_admin() ) {
            return;
        }

        // Only filter on the users screen
        if ( ! ( isset( $current_screen ) && 'users' == $current_screen->id ) ) {
            return;
        }

        $orderby = $_GET['orderby'] ?? '';

        // Only filter if orderby is set to 'media_files'
        if ( 'media_files' == $orderby ) {
            // We need the order - default is ASC
            $order = $_GET['order'] ?? 'ASC';

            // Order the posts by product count
            $query->query_orderby = "ORDER BY ( SELECT COUNT(*) FROM {$wpdb->posts} posts WHERE posts.post_type = 'attachment' AND posts.post_author = {$wpdb->users}.id GROUP BY posts.post_author ) {$order}";
        } elseif ( 'collections' == $orderby ) {
            // We need the order - default is ASC
            $order = $_GET['order'] ?? 'ASC';

            // Order the posts by product count
            $query->query_orderby = "ORDER BY ( SELECT COUNT(*) FROM {$wpdb->posts} posts WHERE posts.post_type = 'collection' AND posts.post_author = {$wpdb->users}.id GROUP BY posts.post_author ) {$order}";
        } elseif ( 'posts' == $orderby ) {
            // We need the order - default is ASC
            $order = $_GET['order'] ?? 'ASC';

            // Order the posts by product count
            $query->query_orderby = "ORDER BY ( SELECT COUNT(*) FROM {$wpdb->posts} posts WHERE posts.post_type = 'post' AND posts.post_author = {$wpdb->users}.id GROUP BY posts.post_author ) {$order}";
        } elseif ( 'forum_posts' == $orderby ) {
            // We need the order - default is ASC
            $order = $_GET['order'] ?? 'ASC';

            // Order the posts by product count
            $query->query_orderby = "ORDER BY ( SELECT COUNT(*) FROM {$wpdb->posts} posts WHERE posts.post_type = 'font_forum' AND posts.post_author = {$wpdb->users}.id GROUP BY posts.post_author ) {$order}";
        } elseif ( 'date' == $orderby ) {
            $order = $_GET['order'] ?? 'ASC';

            $query->query_orderby = "ORDER BY user_registered " . $order;
        } elseif ( 'description' == $orderby ) {
            $order = $_GET['order'] ?? 'ASC';

            // Join the usermeta table to get the 'description' field
            $query->query_from .= " LEFT JOIN {$wpdb->usermeta} ON {$wpdb->users}.ID = {$wpdb->usermeta}.user_id AND meta_key = 'description'";

            // Set the orderby parameter to sort by 'description'
            $query->query_orderby = "ORDER BY {$wpdb->usermeta}.meta_value {$order}";
        }
    }
}

add_action( 'pre_user_query', 'pxf_pre_user_query_action', 1 );

Cái này cũng tương tự như cách Thêm column và sortable column vào bảng danh sách bài viết. Chỉ khác là dùng các tên hook khác nhau áp dụng cho bảng users.

Sử dụng AJAX trong WordPress

AJAX là chữ viết tắt của cụm từ Asynchronous Javascript and XML. AJAX là phương thức trao đổi dữ liệu với máy chủ và cập nhật một hay nhiều phần của trang web, hoàn toàn không reload lại toàn bộ trang.

AJAX được viết bằng Javascript chạy trên client, tức là mỗi browser sẽ chạy độc lập hoàn toàn không ảnh hưởng lẫn nhau. Về mặt kỹ thuật, nó đề cập đến việc sử dụng các đối tượng XmlHttpRequest để tương tác với một máy chủ web thông qua Javascript.

$.ajax({
    type: "GET",
    dataType: "json",
    url: "wp-admin/admin-ajax.php",
    data: {
        action: "lai_dinh_cuong_ajax"
    },
    success: function (response) {
        console.log(response);
    }
});

AJAX là một trong những công cụ giúp chúng ta đem lại cho người dùng trải nghiệm tốt hơn. Khi cần một thay đổi nhỏ thì sẽ không cần load lại cả trang web, làm trang web phải tải lại nhiều thứ không cần thiết.

let ajaxSearch = function () {
    let http,
        cache = [],
        textInput = BODY.find("input[data-xf-init='auto-complete']"),
        timeout = null,
        results = null;

    if (window.XMLHttpRequest) {
        http = new XMLHttpRequest();
    } else {
        http = new ActiveXObject("Microsoft.XMLHTTP");
    }

    $(document).click(function (event) {
        let target = $(event.target),
            results = target.closest(".search-results");

        if (!results || !results.length) {
            $(".search-results").hide();
        }
    });

    BODY.on("click", ".live-search-container .search-results > *", function (e) {
        e.preventDefault();
        let that = this,
            element = $(that),
            container = element.parent(),
            input = container.prev("input");

        container.children().removeClass("is-selected");
        element.addClass("is-selected");
        input.val(element.attr("data-user-name"));
        container.hide();
    });

    function process(search) {
        let interval = setInterval(function () {
            if (http.responseText !== "" || (http.readyState === 4 && http.status === 200)) {
                results.html(http.responseText).show();
                clearInterval(interval);
                cache[search] = http.responseText;
            }
        }, 100);
    }

    textInput.on("keyup", function () {
        liveSearch(this);
    });

    textInput.on("focus click mousedown", function () {
        if (results && results.length) {
            results.hide();
        }
    });

    function liveSearch(element) {
        clearTimeout(timeout);
        let that = element,
            search = that.value;

        element = $(element);
        results = element.parent().find(".search-results");

        if (results && results.length) {
            results.hide();
        }

        if (search.length > 1) {
            timeout = setTimeout(function () {
                if (search in cache) {
                    results.html(cache[search]).show();
                } else {
                    process(search);
                    http.open("GET", GB_FRONTEND.ajaxUrl + "?action=group_buy&do_action=live_search&name=" + that.name + "&data=" + search, true);
                    http.send();
                }
            }, 100);
        } else {
            results.html("").hide();
        }
    }
};

Bên trên là đoạn code ví dụ áp dụng cho live search. Khi gõ từ khóa vào ô tìm kiếm, hệ thống sẽ thực thi AJAX tìm kiếm trong dữ liệu và hiển thị danh sách kết quả gợi ý lên cho người dùng chọn.

jQuery(document).ready(function ($) {
    var body = $("body");

    function hocwp_theme_remove_file_preview(filePreview, inputFile, fesFile, key) {
        filePreview.on("click", ".dz-remove", function (e) {
            e.preventDefault();
            e.stopPropagation();
            inputFile.val(null);
            inputFile.trigger("change");

            if (confirm(pixelify.text.are_you_sure)) {
                var postID = parseInt(filePreview.attr("data-post-id"));

                if (!$.isNumeric(postID)) {
                    postID = parseInt(fesFile.attr("data-post-id"));
                }

                key = key || "font_demos";

                $.ajax({
                    type: "POST",
                    dataType: "JSON",
                    url: pixelify.ajaxUrl,
                    cache: true,
                    data: {
                        action: "hocwp_pxf_remove_file",
                        id: filePreview.attr("data-id"),
                        post_id: postID,
                        key: key
                    },
                    success: function () {
                        filePreview.removeClass("dz-error");
                        filePreview.removeClass("dz-complete");
                        filePreview.removeClass("dz-processing");
                        filePreview.removeClass("has-media");
                        filePreview.attr("data-id", "");
                        filePreview.find("input.media-id").val();

                        var thumbs = fesFile.find(".dz-preview.has-media");

                        if (!thumbs.length) {
                            fesFile.removeClass("has-media");
                        } else {
                            fesFile.addClass("has-media");
                        }

                        fesFile.find(".dz-message").show();

                        filePreview.remove();
                    }
                });
            }
        });
    }

    // Sliders upload
    (function () {
        var fesFile = $("#fes-zf_fes_featured_images"),
            inputFile = fesFile.find("#product-images"),
            dzLabel = fesFile.find(".dz-default.dz-message"),
            myForm = inputFile.closest("form");

        if (myForm.hasClass("image-set")) {
            Cookies.remove("forum_images");
            myForm.removeClass("image-set");
        }

        let forumImages = Cookies.get("forum_images");

        if (forumImages) {
            forumImages = JSON.stringify(forumImages);
        }

        if ("undefined" === typeof forumImages) {
            forumImages = [];
        }

        if (forumImages.length) {
            myForm.find("input[name='tmp_images']").val(Cookies.get("forum_images"));
        }

        dzLabel.on("click", function (e) {
            e.preventDefault();
            e.stopPropagation();

            inputFile.trigger("click");
        });

        fesFile.find(".dz-preview.has-image").each(function () {
            hocwp_theme_remove_image_preview($(this), inputFile, fesFile, null, dzLabel);
        });

        function hocwp_theme_remove_image_preview(imagePreview, inputFile, fesFile, maxImage, dzLabel) {
            imagePreview.on("click", ".dz-remove", function (e) {
                e.preventDefault();
                e.stopPropagation();
                inputFile.val(null);
                inputFile.trigger("change");

                if (confirm(pixelify.text.are_you_sure)) {
                    maxImage = maxImage || parseInt(inputFile.attr("data-max"));

                    var postID = parseInt(imagePreview.attr("data-post-id"));

                    if (!$.isNumeric(postID)) {
                        postID = parseInt(fesFile.attr("data-post-id"));
                    }

                    let removeID = parseInt(imagePreview.attr("data-id"));

                    $.ajax({
                        type: "POST",
                        dataType: "JSON",
                        url: pixelify.ajaxUrl,
                        cache: true,
                        data: {
                            action: "hocwp_pxf_remove_file",
                            id: removeID,
                            post_id: postID,
                            tmp_images: forumImages
                        },
                        success: function (response) {
                            forumImages = response.data.tmp_images;

                            if ("object" === typeof forumImages) {
                                let index = forumImages.indexOf(removeID);

                                if (index !== -1) {
                                    forumImages.splice(index, 1);
                                }
                            }

                            if (forumImages && forumImages.length) {
                                Cookies.set("forum_images", JSON.stringify(forumImages));
                                myForm.find("input[name='tmp_images']").val(Cookies.get("forum_images"));
                            } else {
                                Cookies.remove("forum_images");
                                myForm.find("input[name='tmp_images']").val("");
                            }

                            imagePreview.removeClass("dz-error");
                            imagePreview.removeClass("dz-complete");
                            imagePreview.removeClass("dz-processing");
                            imagePreview.removeClass("has-image");
                            imagePreview.attr("data-id", "");
                            imagePreview.find("input.media-id").val();

                            var thumbs = fesFile.find(".dz-preview.has-image");

                            if (!thumbs.length) {
                                fesFile.removeClass("has-image");
                            } else {
                                fesFile.addClass("has-image");
                            }

                            imagePreview.remove();

                            var previews = fesFile.find(".dz-preview");

                            if (!previews.length) {
                                fesFile.append(pixelify.imagePreview);
                            }

                            if (thumbs.length < maxImage) {
                                dzLabel.show();
                            } else {
                                dzLabel.hide();
                            }
                        }
                    });
                }
            });
        }

        inputFile.on("uploadFile", function () {
            var maxImage = parseInt(inputFile.attr("data-max")),
                thumbs = fesFile.find(".dz-preview.has-image");

            var files = inputFile[0].files,
                i = 0,
                count = files.length,
                maxSize = parseFloat(inputFile.attr("data-max-size"));

            //dzLabel.hide();

            for (i; i < count; i++) {
                var file = files[i];

                if (file) {
                    if (file.size > (maxSize * 1024 * 1024)) {
                        inputFile.val(null);
                        inputFile.trigger("change");

                        return;
                    }

                    (function (file) {
                        var imagePreview = fesFile.find("div.dz-preview:not(.has-image):not(.dz-processing)").first();

                        if (!imagePreview.length) {
                            //fesFile.append(pixelify.imagePreview);
                            //imagePreview = fesFile.find("div.dz-preview:not(.has-image):not(.dz-processing)").first();
                        }

                        imagePreview.addClass("dz-processing");

                        var Upload = function (file) {
                            this.file = file;
                        };

                        Upload.prototype.getType = function () {
                            return this.file.type;
                        };

                        Upload.prototype.getSize = function () {
                            return this.file.size;
                        };

                        Upload.prototype.getName = function () {
                            return this.file.name;
                        };

                        Upload.prototype.doUpload = function () {
                            var that = this;
                            var formData = new FormData();

                            // add assoc key values, this will be posts values
                            formData.append("file", this.file);
                            formData.append("upload_file", true);
                            formData.append("accept", inputFile.attr("accept"));

                            var postID = parseInt(fesFile.attr("data-post-id"));

                            $.ajax({
                                type: "POST",
                                url: pixelify.ajaxUrl + "?action=hocwp_pxf_upload_file&post_id=" + postID,
                                xhr: function () {
                                    var myXhr = $.ajaxSettings.xhr();

                                    if (myXhr.upload) {
                                        myXhr.upload.addEventListener("progress", that.progressHandling, false);
                                    }

                                    return myXhr;
                                },
                                success: function (response) {
                                    // your callback here
                                    imagePreview.removeClass("dz-processing");
                                    imagePreview.addClass("dz-complete");

                                    if (response.success) {
                                        if (response.data.id) {
                                            if (-1 === $.inArray(response.data.id, forumImages)) {
                                                forumImages.push(response.data.id);
                                            }

                                            Cookies.set("forum_images", JSON.stringify(forumImages));
                                            myForm.find("input[name='tmp_images']").val(Cookies.get("forum_images"));

                                            imagePreview.attr("data-id", response.data.id);
                                            imagePreview.find("input.media-id").val(response.data.id);
                                            imagePreview.addClass("has-image");
                                            fesFile.addClass("has-image");
                                            imagePreview.removeClass("dz-processing");

                                            let thumbs = fesFile.find(".dz-preview.has-image");

                                            if (count < maxImage && (!thumbs.length || (thumbs.length + 1) <= maxImage)) {
                                                dzLabel.show();
                                            } else {
                                                dzLabel.hide();
                                            }

                                            inputFile.trigger("uploadCompleted");
                                        }

                                        inputFile.val(null);
                                    } else {
                                        imagePreview.remove();

                                        var previews = fesFile.find(".dz-preview");

                                        if (!previews.length) {
                                            fesFile.removeClass("has-image");
                                            dzLabel.show();
                                        }

                                        if (response.data.message) {
                                            alert(response.data.message);
                                        }
                                    }
                                },
                                error: function (error) {
                                    // handle error
                                },
                                async: true,
                                data: formData,
                                cache: false,
                                contentType: false,
                                processData: false,
                                timeout: 60000
                            });
                        };

                        Upload.prototype.progressHandling = function (event) {
                            let percent = 0,
                                position = event.loaded || event.position,
                                total = event.total;

                            if (event.lengthComputable) {
                                percent = Math.ceil(position / total * 100);
                            }

                            let dzUpload = imagePreview.find(".dz-upload");

                            dzUpload.css({width: percent.toString() + "%"});

                            if (percent <= 100) {
                                imagePreview.addClass("dz-processing");
                            } else {
                                imagePreview.removeClass("dz-processing");
                            }
                        };

                        setTimeout(function () {
                            if (!imagePreview.hasClass("dz-error")) {
                                var upload = new Upload(file);

                                // maybe check size or type here with upload.getSize() and upload.getType()

                                // execute upload
                                upload.doUpload();
                            } else {
                                imagePreview.removeClass("dz-processing");
                            }
                        }, 2000);

                        var maxWidth = parseInt(inputFile.attr("data-max-width")),
                            maxHeight = parseInt(inputFile.attr("data-max-height"));

                        (function (maxWidth, maxHeight, imagePreview, file) {
                            var img = new Image();

                            img.src = window.URL.createObjectURL(file);

                            img.onload = function () {
                                var width = img.width,
                                    height = img.height;

                                window.URL.revokeObjectURL(img.src);

                                if (width > maxHeight || height > maxHeight) {
                                    imagePreview.removeClass("dz-processing");
                                    imagePreview.addClass("dz-error");
                                }
                            };

                            var reader = new FileReader();

                            reader.onload = function (e) {
                                imagePreview.find(".dz-image img").attr("src", e.target.result).show();
                                imagePreview.show();
                            };

                            reader.readAsDataURL(file);

                            hocwp_theme_remove_image_preview(imagePreview, inputFile, fesFile, maxImage, dzLabel);
                        })(maxWidth, maxHeight, imagePreview, file);
                    })(file);
                }
            }
        });

        inputFile.on("change", function () {
            dzLabel.hide();

            if (this.files && this.files.length) {
                var maxImage = parseInt(inputFile.attr("data-max")),
                    thumbs = fesFile.find(".dz-preview.has-image");

                if (this.files.length <= maxImage && (!thumbs.length || (thumbs.length + this.files.length) <= maxImage)) {
                    // Do upload file instantly after file chosen.
                    var files = this.files,
                        i = 0,
                        count = files.length,
                        maxSize = parseFloat(inputFile.attr("data-max-size"));

                    //dzLabel.hide();

                    for (i; i < count; i++) {
                        var file = files[i];

                        if (file) {
                            if (file.size > (maxSize * 1024 * 1024)) {
                                inputFile.val(null);
                                inputFile.trigger("change");

                                return;
                            }

                            (function (file) {
                                var imagePreview = fesFile.find("div.dz-preview:not(.has-image):not(.dz-processing)").first();

                                if (!imagePreview.length) {
                                    fesFile.append(pixelify.imagePreview);
                                    imagePreview = fesFile.find("div.dz-preview:not(.has-image):not(.dz-processing)").first();
                                }

                                //imagePreview.addClass("dz-processing");

                                var maxWidth = parseInt(inputFile.attr("data-max-width")),
                                    maxHeight = parseInt(inputFile.attr("data-max-height"));

                                (function (maxWidth, maxHeight, imagePreview, file) {
                                    var img = new Image();

                                    img.src = window.URL.createObjectURL(file);

                                    img.onload = function () {
                                        var width = img.width,
                                            height = img.height;

                                        window.URL.revokeObjectURL(img.src);

                                        if (width > maxHeight || height > maxHeight) {
                                            imagePreview.removeClass("dz-processing");
                                            imagePreview.addClass("dz-error");
                                        }
                                    };

                                    var reader = new FileReader();

                                    reader.onload = function (e) {
                                        imagePreview.find(".dz-image img").attr("src", e.target.result).show();
                                        imagePreview.show();
                                    };

                                    reader.readAsDataURL(file);

                                    hocwp_theme_remove_image_preview(imagePreview, inputFile, fesFile, maxImage, dzLabel);
                                })(maxWidth, maxHeight, imagePreview, file);
                            })(file);
                        }
                    }
                } else {
                    alert("You cannot upload more than 5 images");
                    inputFile.val(null);
                    inputFile.trigger("change");

                    if (this.files.length === 1) {
                        dzLabel.hide();
                    } else {
                        dzLabel.show();
                    }
                }
            }
        });

        body.on("click", ".add-forum form[name='fes-submission-form'] input[type='submit']", function () {
            let that = this,
                element = $(that),
                form = myForm,
                upload = form.find("input[name='product_images']");

            if (upload[0].files.length === 0) {
                form.addClass("upload-done");
            }
        });

        function continueSubmitForm(element) {
            setTimeout(function () {
                element.addClass("upload-done");
                element.unbind("submit").submit();
                element.find("input[type='submit']").click();
            }, 2000);
        }

        $(".add-forum form[name='fes-submission-form']").on("submit", function (e) {
            let that = this,
                element = $(that),
                upload = element.find("input[name='product_images']");

            if (!element.hasClass("upload-done")) {
                e.preventDefault();
            }

            upload.on("uploadCompleted", function () {
                continueSubmitForm(element);
            });

            if (upload[0].files.length) {
                upload.trigger("uploadFile");
            } else {
                continueSubmitForm(element);
            }

            return true;
        });
    })();
});

Thêm một đoạn code mô tả quá trình tải tệp lên máy chủ bằng AJAX. Trong quá trình tải lện sẽ cập nhật trạng thái phần trằm hoàn thành. Sau khi hoàn tất quá trình tải lên, sẽ ẩn thanh trạng thái và hiển thị bản xem thử cho người dùng.

Singleton trong PHP

Đôi khi bạn muốn tạo 1 class nhưng không cho phép nó được khởi tạo nhiều lần, mà class này chỉ được khởi tạo 1 lần và dùng đi dùng lại thì bạn hãy tìm từ khóa Singleton.

/**
 * The Singleton class defines the `GetInstance` method that serves as an
 * alternative to constructor and lets clients access the same instance of this
 * class over and over.
 */
class Singleton {
    /**
     * The Singleton's instance is stored in a static field. This field is an
     * array, because we'll allow our Singleton to have subclasses. Each item in
     * this array will be an instance of a specific Singleton's subclass. You'll
     * see how this works in a moment.
     */
    private static $instance = null;

    /**
     * The Singleton's constructor should always be private to prevent direct
     * construction calls with the `new` operator.
     */
    protected function __construct() {
    }

    /**
     * Singletons should not be cloneable.
     */
    protected function __clone() {
    }

    /**
     * Singletons should not be restorable from strings.
     */
    public function __wakeup() {
        throw new \Exception( "Cannot unserialize a singleton." );
    }

    /**
     * This is the static method that controls the access to the singleton
     * instance. On the first run, it creates a singleton object and places it
     * into the static field. On subsequent runs, it returns the client existing
     * object stored in the static field.
     *
     * This implementation lets you subclass the Singleton class while keeping
     * just one instance of each subclass around.
     */
    public static function get_instance(): Singleton {
        if ( ! ( self::$instance instanceof self ) ) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * Finally, any singleton should define some business logic, which can be
     * executed on its instance.
     */
    public function some_function() {
        // ...
    }
}

/**
 * The client code.
 */
function client_call_code() {
    $s1 = Singleton::get_instance();
    $s2 = Singleton::get_instance();

    if ( $s1 === $s2 ) {
        return "Singleton works, both variables contain the same instance.";
    }

    return "Singleton failed, variables contain different instances.";
}

error_log( client_call_code() );

Ok, như vậy bạn không thể khởi tạo new Singleton() cho các biến mà chỉ có thể dùng thông qua hàm Singleton::get_instance(). Dù bạn có gọi bao nhiêu lần thì đối tượng Singleton cũng chỉ là duy nhất.

Quy tắc đặt tên trong Javascript

Quy tắc đặt tên áp dụng trong khi viết mã Javascript.

// Naming Rules in Javascript

// Variables
let fullName = "Lai Dinh Cuong";

// Boolean variables
let isStudent = false;
let hasMoney = false;

// Functions
function getInfo() {
    return "ABC";
}

// Constants
const HEIGHT = 1720;
const BIRTH_DAY = 19900924;

// Classes
class SleepCalculator {
    constructor(bedtime, wakeupTime) {
        this.bedtime = bedtime;
        this.wakeupTime = wakeupTime;
    }

    getInfo() {
        return [
            this.bedtime,
            this.wakeupTime
        ]
    }
}

let calculator = new SleepCalculator("21:45", "05:30");
console.log(calculator.getInfo());

// Private functions
class Cat {
    constructor(name, color, numLegs) {
        this.name = name;
        this.color = color;
        this.numLegs = numLegs;
        this.detail = this._getName();
    }

    _getName() {
        return this.name + ", legs: " + this.numLegs + ", color: " + this.color;
    }
}

let cat = new Cat("Tom", "white", 4);
console.log(cat);

 

Cách bật ionCube Loader PHP Extension trên cPanel

Khi bạn mua giao diện hoặc plugin thì lâu lâu sẽ có một số sản phẩm yêu cầu gói mở rộng ionCube Loader để hoạt động. Trong bài viết này, mình sẽ hướng dẫn cho bạn cách bật ionCube Loader PHP Extension trên cPanel.

Hướng dẫn kích hoạt ionCube Loader PHP Extension
Hướng dẫn kích hoạt ionCube Loader PHP Extension

Cách bật ionCube Loader PHP Extension trên cPanel

Cái này cũng tương đối đơn giản thôi. Thường thì các nhà cung cấp hosting mặc định đã cài đặt và bật cho bạn rồi. Nhưng nếu trang web báo lỗi thiếu ionCube Loader Extension thì bạn sẽ làm như sau.

1. Đăng nhập vào cPanel và kiểm tra xem có phần chọn Select PHP Version hay không, nếu có thì nhấn vào, không thì liên hệ nhà cung cấp hosting.

Chọn phiên bản PHP và gói mở rộng
Chọn phiên bản PHP và gói mở rộng

2. Sau khi vào phần chọn phiên bản PHP rồi thì chọn phiên bản PHP cho phù hợp. Hiện tại WordPress mới nhất đã hỗ trợ phiên bản PHP 8.2 trở lên nên bạn cứ chọn PHP mới nhất mà chiến. Cái này sẽ giúp cải thiện hiệu năng và nâng cao bảo mật.

Sau khi chọn xong phiên bản PHP cần dùng và nhấn Apply rồi thì chuyển sang tab Extensions như hình đầu bài. Tick chọn vào ô ioncube_loader như hình mũi tên mình đánh dấu màu vàng. Xong xuôi hết rồi thì bạn quay lại trang web để kiểm tra.

Như vậy là mình đã hướng dẫn cho các bạn cách bật ionCube Loader PHP Extension trên cPanel. Đối với cách bật ionCube Loader PHP Extension trên các hosting khác hoặc VPS, Server,… thì bạn có thể tìm kiếm cách làm tương tự trên Google nhé.

Nếu bạn có bất kỳ thắc mắc nào cần giải đáp, hãy gửi liên hệ ngay cho HocWP Team để được tư vấn nhé. Bên cạnh đó, bạn cũng có thể đăng ký theo dõi kênh YouTube Học WordPress để cập nhật kiến thức về quản lý và phát triển website nhé.

Ví dụ code form thanh toán Stripe

Nếu bạn đang làm code thanh toán qua Stripe, bạn muốn tùy chỉnh form thanh toán thì có thể tham khảo tài liệu của nhà cung cấp.

Ví dụ Stripe payment form
Ví dụ Stripe payment form

Ví dụ: https://stripe.dev/elements-examples/

Trong này có nhiều kiểu form cho bạn tùy chọn. Có thể áp dụng form nhập thẻ Visa/Debit chung trong 1 trường hoặc tách các trường số thẻ, ngày hết hạn và CVC.

Truy vấn lấy giá trị lớn nhất, giá trị nhỏ nhất trong bảng meta WordPress

Trong ví dụ này, mình có một cột meta_key có giá trị là rent, và một cột meta_value có giá trị là con số với kiểu dữ liệu là LONGTEXT. Các đối tượng rent sẽ chứa một con số thực thể hiện giá cho thuê.

Để lấy được giá cao nhất và giá thấp nhất, đầu tiên bạn phải ép kiểu dữ liệu sang kiểu SIGNED hoặc UNSIGNED, tuy nhiên trong ví dụ này giá cho thuê chỉ là giá trị dương nên mình chọn kiểu dữ liệu để chuyển đổi là UNSIGNED.

global $wpdb;

$max = $wpdb->get_var( "SELECT MAX(CAST(meta_value AS UNSIGNED)) FROM $wpdb->postmeta WHERE meta_key = 'rent'" );
$min = $wpdb->get_var( "SELECT MIN(CAST(meta_value AS UNSIGNED)) FROM $wpdb->postmeta WHERE meta_key = 'rent'" );

Hàm get_var thực hiện 1 truy vấn và trả về 1 giá trị nhất định, ở đây là giá cho thuê cao nhất và thấp nhất.

Mã điện thoại quốc tế và Việt Nam bằng PHP

Danh sách mã quốc gia và mã điện thoại của 252 quốc gia trên toàn thế giới bằng PHP.

$country_list = array(
    "AF" => array("name" => "Afghanistan", "phone" => 93),
    "AX" => array("name" => "Aland Islands", "phone" => 358),
    "AL" => array("name" => "Albania", "phone" => 355),
    "DZ" => array("name" => "Algeria", "phone" => 213),
    "AS" => array("name" => "American Samoa", "phone" => 1684),
    "AD" => array("name" => "Andorra", "phone" => 376),
    "AO" => array("name" => "Angola", "phone" => 244),
    "AI" => array("name" => "Anguilla", "phone" => 1264),
    "AQ" => array("name" => "Antarctica", "phone" => 672),
    "AG" => array("name" => "Antigua and Barbuda", "phone" => 1268),
    "AR" => array("name" => "Argentina", "phone" => 54),
    "AM" => array("name" => "Armenia", "phone" => 374),
    "AW" => array("name" => "Aruba", "phone" => 297),
    "AU" => array("name" => "Australia", "phone" => 61),
    "AT" => array("name" => "Austria", "phone" => 43),
    "AZ" => array("name" => "Azerbaijan", "phone" => 994),
    "BS" => array("name" => "Bahamas", "phone" => 1242),
    "BH" => array("name" => "Bahrain", "phone" => 973),
    "BD" => array("name" => "Bangladesh", "phone" => 880),
    "BB" => array("name" => "Barbados", "phone" => 1246),
    "BY" => array("name" => "Belarus", "phone" => 375),
    "BE" => array("name" => "Belgium", "phone" => 32),
    "BZ" => array("name" => "Belize", "phone" => 501),
    "BJ" => array("name" => "Benin", "phone" => 229),
    "BM" => array("name" => "Bermuda", "phone" => 1441),
    "BT" => array("name" => "Bhutan", "phone" => 975),
    "BO" => array("name" => "Bolivia", "phone" => 591),
    "BQ" => array("name" => "Bonaire, Sint Eustatius and Saba", "phone" => 599),
    "BA" => array("name" => "Bosnia and Herzegovina", "phone" => 387),
    "BW" => array("name" => "Botswana", "phone" => 267),
    "BV" => array("name" => "Bouvet Island", "phone" => 55),
    "BR" => array("name" => "Brazil", "phone" => 55),
    "IO" => array("name" => "British Indian Ocean Territory", "phone" => 246),
    "BN" => array("name" => "Brunei Darussalam", "phone" => 673),
    "BG" => array("name" => "Bulgaria", "phone" => 359),
    "BF" => array("name" => "Burkina Faso", "phone" => 226),
    "BI" => array("name" => "Burundi", "phone" => 257),
    "KH" => array("name" => "Cambodia", "phone" => 855),
    "CM" => array("name" => "Cameroon", "phone" => 237),
    "CA" => array("name" => "Canada", "phone" => 1),
    "CV" => array("name" => "Cape Verde", "phone" => 238),
    "KY" => array("name" => "Cayman Islands", "phone" => 1345),
    "CF" => array("name" => "Central African Republic", "phone" => 236),
    "TD" => array("name" => "Chad", "phone" => 235),
    "CL" => array("name" => "Chile", "phone" => 56),
    "CN" => array("name" => "China", "phone" => 86),
    "CX" => array("name" => "Christmas Island", "phone" => 61),
    "CC" => array("name" => "Cocos (Keeling) Islands", "phone" => 672),
    "CO" => array("name" => "Colombia", "phone" => 57),
    "KM" => array("name" => "Comoros", "phone" => 269),
    "CG" => array("name" => "Congo", "phone" => 242),
    "CD" => array("name" => "Congo, Democratic Republic of the Congo", "phone" => 242),
    "CK" => array("name" => "Cook Islands", "phone" => 682),
    "CR" => array("name" => "Costa Rica", "phone" => 506),
    "CI" => array("name" => "Cote D'Ivoire", "phone" => 225),
    "HR" => array("name" => "Croatia", "phone" => 385),
    "CU" => array("name" => "Cuba", "phone" => 53),
    "CW" => array("name" => "Curacao", "phone" => 599),
    "CY" => array("name" => "Cyprus", "phone" => 357),
    "CZ" => array("name" => "Czech Republic", "phone" => 420),
    "DK" => array("name" => "Denmark", "phone" => 45),
    "DJ" => array("name" => "Djibouti", "phone" => 253),
    "DM" => array("name" => "Dominica", "phone" => 1767),
    "DO" => array("name" => "Dominican Republic", "phone" => 1809),
    "EC" => array("name" => "Ecuador", "phone" => 593),
    "EG" => array("name" => "Egypt", "phone" => 20),
    "SV" => array("name" => "El Salvador", "phone" => 503),
    "GQ" => array("name" => "Equatorial Guinea", "phone" => 240),
    "ER" => array("name" => "Eritrea", "phone" => 291),
    "EE" => array("name" => "Estonia", "phone" => 372),
    "ET" => array("name" => "Ethiopia", "phone" => 251),
    "FK" => array("name" => "Falkland Islands (Malvinas)", "phone" => 500),
    "FO" => array("name" => "Faroe Islands", "phone" => 298),
    "FJ" => array("name" => "Fiji", "phone" => 679),
    "FI" => array("name" => "Finland", "phone" => 358),
    "FR" => array("name" => "France", "phone" => 33),
    "GF" => array("name" => "French Guiana", "phone" => 594),
    "PF" => array("name" => "French Polynesia", "phone" => 689),
    "TF" => array("name" => "French Southern Territories", "phone" => 262),
    "GA" => array("name" => "Gabon", "phone" => 241),
    "GM" => array("name" => "Gambia", "phone" => 220),
    "GE" => array("name" => "Georgia", "phone" => 995),
    "DE" => array("name" => "Germany", "phone" => 49),
    "GH" => array("name" => "Ghana", "phone" => 233),
    "GI" => array("name" => "Gibraltar", "phone" => 350),
    "GR" => array("name" => "Greece", "phone" => 30),
    "GL" => array("name" => "Greenland", "phone" => 299),
    "GD" => array("name" => "Grenada", "phone" => 1473),
    "GP" => array("name" => "Guadeloupe", "phone" => 590),
    "GU" => array("name" => "Guam", "phone" => 1671),
    "GT" => array("name" => "Guatemala", "phone" => 502),
    "GG" => array("name" => "Guernsey", "phone" => 44),
    "GN" => array("name" => "Guinea", "phone" => 224),
    "GW" => array("name" => "Guinea-Bissau", "phone" => 245),
    "GY" => array("name" => "Guyana", "phone" => 592),
    "HT" => array("name" => "Haiti", "phone" => 509),
    "HM" => array("name" => "Heard Island and McDonald Islands", "phone" => 0),
    "VA" => array("name" => "Holy See (Vatican City State)", "phone" => 39),
    "HN" => array("name" => "Honduras", "phone" => 504),
    "HK" => array("name" => "Hong Kong", "phone" => 852),
    "HU" => array("name" => "Hungary", "phone" => 36),
    "IS" => array("name" => "Iceland", "phone" => 354),
    "IN" => array("name" => "India", "phone" => 91),
    "ID" => array("name" => "Indonesia", "phone" => 62),
    "IR" => array("name" => "Iran, Islamic Republic of", "phone" => 98),
    "IQ" => array("name" => "Iraq", "phone" => 964),
    "IE" => array("name" => "Ireland", "phone" => 353),
    "IM" => array("name" => "Isle of Man", "phone" => 44),
    "IL" => array("name" => "Israel", "phone" => 972),
    "IT" => array("name" => "Italy", "phone" => 39),
    "JM" => array("name" => "Jamaica", "phone" => 1876),
    "JP" => array("name" => "Japan", "phone" => 81),
    "JE" => array("name" => "Jersey", "phone" => 44),
    "JO" => array("name" => "Jordan", "phone" => 962),
    "KZ" => array("name" => "Kazakhstan", "phone" => 7),
    "KE" => array("name" => "Kenya", "phone" => 254),
    "KI" => array("name" => "Kiribati", "phone" => 686),
    "KP" => array("name" => "Korea, Democratic People's Republic of", "phone" => 850),
    "KR" => array("name" => "Korea, Republic of", "phone" => 82),
    "XK" => array("name" => "Kosovo", "phone" => 383),
    "KW" => array("name" => "Kuwait", "phone" => 965),
    "KG" => array("name" => "Kyrgyzstan", "phone" => 996),
    "LA" => array("name" => "Lao People's Democratic Republic", "phone" => 856),
    "LV" => array("name" => "Latvia", "phone" => 371),
    "LB" => array("name" => "Lebanon", "phone" => 961),
    "LS" => array("name" => "Lesotho", "phone" => 266),
    "LR" => array("name" => "Liberia", "phone" => 231),
    "LY" => array("name" => "Libyan Arab Jamahiriya", "phone" => 218),
    "LI" => array("name" => "Liechtenstein", "phone" => 423),
    "LT" => array("name" => "Lithuania", "phone" => 370),
    "LU" => array("name" => "Luxembourg", "phone" => 352),
    "MO" => array("name" => "Macao", "phone" => 853),
    "MK" => array("name" => "Macedonia, the Former Yugoslav Republic of", "phone" => 389),
    "MG" => array("name" => "Madagascar", "phone" => 261),
    "MW" => array("name" => "Malawi", "phone" => 265),
    "MY" => array("name" => "Malaysia", "phone" => 60),
    "MV" => array("name" => "Maldives", "phone" => 960),
    "ML" => array("name" => "Mali", "phone" => 223),
    "MT" => array("name" => "Malta", "phone" => 356),
    "MH" => array("name" => "Marshall Islands", "phone" => 692),
    "MQ" => array("name" => "Martinique", "phone" => 596),
    "MR" => array("name" => "Mauritania", "phone" => 222),
    "MU" => array("name" => "Mauritius", "phone" => 230),
    "YT" => array("name" => "Mayotte", "phone" => 262),
    "MX" => array("name" => "Mexico", "phone" => 52),
    "FM" => array("name" => "Micronesia, Federated States of", "phone" => 691),
    "MD" => array("name" => "Moldova, Republic of", "phone" => 373),
    "MC" => array("name" => "Monaco", "phone" => 377),
    "MN" => array("name" => "Mongolia", "phone" => 976),
    "ME" => array("name" => "Montenegro", "phone" => 382),
    "MS" => array("name" => "Montserrat", "phone" => 1664),
    "MA" => array("name" => "Morocco", "phone" => 212),
    "MZ" => array("name" => "Mozambique", "phone" => 258),
    "MM" => array("name" => "Myanmar", "phone" => 95),
    "NA" => array("name" => "Namibia", "phone" => 264),
    "NR" => array("name" => "Nauru", "phone" => 674),
    "NP" => array("name" => "Nepal", "phone" => 977),
    "NL" => array("name" => "Netherlands", "phone" => 31),
    "AN" => array("name" => "Netherlands Antilles", "phone" => 599),
    "NC" => array("name" => "New Caledonia", "phone" => 687),
    "NZ" => array("name" => "New Zealand", "phone" => 64),
    "NI" => array("name" => "Nicaragua", "phone" => 505),
    "NE" => array("name" => "Niger", "phone" => 227),
    "NG" => array("name" => "Nigeria", "phone" => 234),
    "NU" => array("name" => "Niue", "phone" => 683),
    "NF" => array("name" => "Norfolk Island", "phone" => 672),
    "MP" => array("name" => "Northern Mariana Islands", "phone" => 1670),
    "NO" => array("name" => "Norway", "phone" => 47),
    "OM" => array("name" => "Oman", "phone" => 968),
    "PK" => array("name" => "Pakistan", "phone" => 92),
    "PW" => array("name" => "Palau", "phone" => 680),
    "PS" => array("name" => "Palestinian Territory, Occupied", "phone" => 970),
    "PA" => array("name" => "Panama", "phone" => 507),
    "PG" => array("name" => "Papua New Guinea", "phone" => 675),
    "PY" => array("name" => "Paraguay", "phone" => 595),
    "PE" => array("name" => "Peru", "phone" => 51),
    "PH" => array("name" => "Philippines", "phone" => 63),
    "PN" => array("name" => "Pitcairn", "phone" => 64),
    "PL" => array("name" => "Poland", "phone" => 48),
    "PT" => array("name" => "Portugal", "phone" => 351),
    "PR" => array("name" => "Puerto Rico", "phone" => 1787),
    "QA" => array("name" => "Qatar", "phone" => 974),
    "RE" => array("name" => "Reunion", "phone" => 262),
    "RO" => array("name" => "Romania", "phone" => 40),
    "RU" => array("name" => "Russian Federation", "phone" => 7),
    "RW" => array("name" => "Rwanda", "phone" => 250),
    "BL" => array("name" => "Saint Barthelemy", "phone" => 590),
    "SH" => array("name" => "Saint Helena", "phone" => 290),
    "KN" => array("name" => "Saint Kitts and Nevis", "phone" => 1869),
    "LC" => array("name" => "Saint Lucia", "phone" => 1758),
    "MF" => array("name" => "Saint Martin", "phone" => 590),
    "PM" => array("name" => "Saint Pierre and Miquelon", "phone" => 508),
    "VC" => array("name" => "Saint Vincent and the Grenadines", "phone" => 1784),
    "WS" => array("name" => "Samoa", "phone" => 684),
    "SM" => array("name" => "San Marino", "phone" => 378),
    "ST" => array("name" => "Sao Tome and Principe", "phone" => 239),
    "SA" => array("name" => "Saudi Arabia", "phone" => 966),
    "SN" => array("name" => "Senegal", "phone" => 221),
    "RS" => array("name" => "Serbia", "phone" => 381),
    "CS" => array("name" => "Serbia and Montenegro", "phone" => 381),
    "SC" => array("name" => "Seychelles", "phone" => 248),
    "SL" => array("name" => "Sierra Leone", "phone" => 232),
    "SG" => array("name" => "Singapore", "phone" => 65),
    "SX" => array("name" => "St Martin", "phone" => 721),
    "SK" => array("name" => "Slovakia", "phone" => 421),
    "SI" => array("name" => "Slovenia", "phone" => 386),
    "SB" => array("name" => "Solomon Islands", "phone" => 677),
    "SO" => array("name" => "Somalia", "phone" => 252),
    "ZA" => array("name" => "South Africa", "phone" => 27),
    "GS" => array("name" => "South Georgia and the South Sandwich Islands", "phone" => 500),
    "SS" => array("name" => "South Sudan", "phone" => 211),
    "ES" => array("name" => "Spain", "phone" => 34),
    "LK" => array("name" => "Sri Lanka", "phone" => 94),
    "SD" => array("name" => "Sudan", "phone" => 249),
    "SR" => array("name" => "Suriname", "phone" => 597),
    "SJ" => array("name" => "Svalbard and Jan Mayen", "phone" => 47),
    "SZ" => array("name" => "Swaziland", "phone" => 268),
    "SE" => array("name" => "Sweden", "phone" => 46),
    "CH" => array("name" => "Switzerland", "phone" => 41),
    "SY" => array("name" => "Syrian Arab Republic", "phone" => 963),
    "TW" => array("name" => "Taiwan, Province of China", "phone" => 886),
    "TJ" => array("name" => "Tajikistan", "phone" => 992),
    "TZ" => array("name" => "Tanzania, United Republic of", "phone" => 255),
    "TH" => array("name" => "Thailand", "phone" => 66),
    "TL" => array("name" => "Timor-Leste", "phone" => 670),
    "TG" => array("name" => "Togo", "phone" => 228),
    "TK" => array("name" => "Tokelau", "phone" => 690),
    "TO" => array("name" => "Tonga", "phone" => 676),
    "TT" => array("name" => "Trinidad and Tobago", "phone" => 1868),
    "TN" => array("name" => "Tunisia", "phone" => 216),
    "TR" => array("name" => "Turkey", "phone" => 90),
    "TM" => array("name" => "Turkmenistan", "phone" => 7370),
    "TC" => array("name" => "Turks and Caicos Islands", "phone" => 1649),
    "TV" => array("name" => "Tuvalu", "phone" => 688),
    "UG" => array("name" => "Uganda", "phone" => 256),
    "UA" => array("name" => "Ukraine", "phone" => 380),
    "AE" => array("name" => "United Arab Emirates", "phone" => 971),
    "GB" => array("name" => "United Kingdom", "phone" => 44),
    "US" => array("name" => "United States", "phone" => 1),
    "UM" => array("name" => "United States Minor Outlying Islands", "phone" => 1),
    "UY" => array("name" => "Uruguay", "phone" => 598),
    "UZ" => array("name" => "Uzbekistan", "phone" => 998),
    "VU" => array("name" => "Vanuatu", "phone" => 678),
    "VE" => array("name" => "Venezuela", "phone" => 58),
    "VN" => array("name" => "Viet Nam", "phone" => 84),
    "VG" => array("name" => "Virgin Islands, British", "phone" => 1284),
    "VI" => array("name" => "Virgin Islands, U.s.", "phone" => 1340),
    "WF" => array("name" => "Wallis and Futuna", "phone" => 681),
    "EH" => array("name" => "Western Sahara", "phone" => 212),
    "YE" => array("name" => "Yemen", "phone" => 967),
    "ZM" => array("name" => "Zambia", "phone" => 260),
    "ZW" => array("name" => "Zimbabwe", "phone" => 263)
);

 

Cập nhật giỏ hàng và đếm giỏ hàng khi thêm sản phẩm

Khi thêm 1 sản phẩm vào giỏ hàng bằng AJAX, hệ thống sẽ tự động đếm số lượng của mini cart và cập nhật lại danh sách sản phẩm trong mini cart.

<div class="basket-item-count">
  <span class="cart-items-count">
  <?php
    echo WC()->cart->get_cart_contents_count();
  ?>
</span>
</div>

Mặc định, nếu dùng thẻ a có class là cart-contents sẽ được đếm tự động. Trong ví dụ bên trên sẽ sử dụng thẻ span có class là cart-items-count.

<div class="widget_shopping_cart_content">
   <?php
     echo woocommerce_mini_cart();
   ?>
</div>

Đối với nội dung của giỏ hàng, sử dụng thẻ div với class là widget_shopping_cart_content.

add_filter( 'woocommerce_add_to_cart_fragments', 'cart_count_fragments', 10, 1 );

function cart_count_fragments( $fragments ) {
    $fragments['div.basket-item-count'] = '<div class="basket-item-count"><span class="cart-items-count">' . WC()->cart->get_cart_contents_count() . '</span></div>';
    return $fragments;
}

 

Khi WooCommerce cập nhật cart fragments, hệ thống sẽ thêm thẻ div.basket-item-count vào trong mảng với nội dung đúng với đoạn code đầu tiên trong bài này.