תשרי

לילות כימים: פרק 12.125 – תמונה שווה אלף מילים

אם אתם חדשים פה אז ברוכים הבאים! הפוסט הזה הוא חלק מסדרה שבהם אני מתארת את עבודתי התחביבית על האתר של עמותת כמוך. מוזמנים לקרוא את כל הפרקים כאן.

השבוע אתר עמותת כמוך בחופשת חג, ולכן לא מתפרסמת שם הרשומה השבועית שלי. א-ב-ל אצלי לא רציתי לעשות הפסקה כי הבלוג שלי לא בחופשה, ודווקא כיף לי שהסדרה הזאת מתפרסמת (גם לכם?). אמנם כמו שסיפרתי בפרק הקודם, אני כותבת את הפרקים זמן רב מראש, אבל ההחלטה על הפרק הזה גמלה אצלי באופן ספונטני ומיידי, לפני יומיים. רציתי לנצל את ההזדמנות ולכתוב פוסט תכנותי על אחד הדברים שפיתחתי לכמוך אבל לא ידעתי על מה. שאלה תמימה בפייסבוק (מספר 2 בפוסט הנ”ל) נתנה לי רעיון (איזה תזמון! מה הסיכוי?!) והנה אנחנו כאן.

לעבור למנגנון מובנה

האתר של עמותת כמוך פוּתח הרבה לפני פרוץ ה-featured image – התמונה הראשית של פוסטים, והדרך שלהם להוסיף תמונה שתלווה פוסט היתה באמצעות שדה מיוחד – היו מעלים תמונה לאתר, ואת הכתובת שלה הכניסו לשדה המיוחד שהיה שדה טקסט, והקוד של התבנית היה מושך את הכתובת משם לתוך ה-src של תגית img. במקרה שאין שדה מיוחד עדיין לא אלמן ישראל כי בד”כ יש תמונות בפוסטים עצמם (למשל בפוסטים של לילות כימים, תמיד מופיעה תמונת הבלוג – הינשוף), אז מופיעה התמונה הראשונה כתמונה ראשית.
בפיתוח החדש רציתי שנשתמש בפיצ’ר של תמונה ראשית – גם מפני אני תמיד בעד להשתמש בכלים שהמערכת נותנת בברירת מחדל וגם כי תהליך העבודה הקודם היה מייגע, במיוחד למי שאינו מתכנת. אבל איך לעשות זאת? הרי צריך יהיה לשייך את כל התמונות מהשדות המיוחדים או מהפוסט, כך שיוגדרו כתמונות המלוות של הפוסטים!
התחלתי לחפש קצת ברשת, ולבסוף יצא לי קוד שעובד. הוא לא מושלם מכל מיני סיבות – למשל לא הקפדתי על תחיליות קבועות לשמות הפונקציות, או למשל הוא נמצא בקוד של התבנית (אם כי מבודד בקובץ ש-functions.php עושה לו include) בעוד שהכי טוב היה לוּ יצרתי ממנו תוסף – אבל אני משתפת אותו בכל זאת, ראשית מפני שהוא עובד ועונה על צורך שלפעמים עולה, ושנית מפני שאולי תהיינה תגובות – לטוב או למוטב – שתעודדנה אותי לשפר את הטעון שיפור.

נתחיל מהשימוש: יצרתי פונקציה בשם kamoha_show_homepage_thumbnail שמקבלת כפרמטר את גודל ה-thumbnail, ואני משתמשת בה בכל מקום בתבנית שבו הייתי משתמשת ב-get_the_post_thumbnail. זה גם היופי בשימוש בה – לא צריך להריץ אותה במיוחד רק כדי לעשות את השיוך, אלא היא רצה תמיד בכל מקום שבו מוצגת תמונה ראשית. באתר של כמוך זה אומר בכל מקום שהוא לא דף הפוסט עצמו: הדף הראשי, דף קטגוריה, דף תגית, והחיפוש. מצד שני, מובן שאין צורך לעשות את השיוך הזה כל פעם, ולכן הבדיקה הראשונה בפונקציה היא שאם יש לפוסט תמונה ראשית – מציגים אותה ושלום שלום. רק אם אין לה, מתחיל תהליך זיהוי התמונה, העלאתה במקרה הצורך, ושיוכה לפוסט.
אסביר את הקוד שורה אחרי שורה, ובסוף ישנו קישור לכל הקובץ בגיטהאב. בעצם הנה גם כאן היה קישור 😉 .

מתחילים

הפונקציה מתחילה בבדיקה אם כבר יש תמונה ראשית. אם היא ישנה, מציגים אותה. שאר הקוד הוא else, לכן בעצם במקרה הזה אנחנו עוזבים את הפונקציה מיד:

if ( has_post_thumbnail() ) { 
 echo get_the_post_thumbnail( null, $thumb_size );
}

אם אין תמונה ראשית, נחפש בשדה מיוחד

המשך הקוד מטפל במצב שבו עדיין אין לפוסט תמונה ראשית.
אם אין תמונה ראשית, בודקים אם יש שדה מיוחד בשם thumb ולוקחים את כתובת התמונה משם:

else {
 /* if the post doesn't have a thumbnail, the first place to look for it is the userfiled 'thumb'.
 if there is no img there, then get the first image in the post */
 
 $imgsrc = get_post_meta( $post_id, 'thumb', true ); // first get the value of thumb

אם לא מצאנו שדה מיוחד, ניקח את התמונה הראשונה שמופיעה בפוסט עצמו

אם לא מצאנו ערך בשדה (השדה בעצם לא קיים), אז נחפש בפוסט את התמונה הראשונה וניקח את הכתובת שלה:

if ( empty( $imgsrc ) ) {
 
 /* if no value in thumb, get the first image from the post */
 $imgsrc = kamoha_catch_that_image();
 }

הפונקציה kamoha_catch_that_image מחפשת ולוכדת את התמונה הראשונה בפוסט. מצאה לי אותה רחלי ב-css tricks והיא שימושית להפליא. לא הייתי זקוקה לחלק מהקוד שלה שבודק אם יש thumbnail כי אני מבצעת את הבדיקה הזאת מחוץ לפונקציה (אבל כדאי להעיף מבט, קריס קויר טוען שהלוגיקה שהוא השתמש בה לבדיקה יותר מהימנה מ-has_post_thumbnail), ומצד שני, אם לא נמצאת תמונה, אני מכניסה תמונת ברירת מחדל (בחרתי את סמליל האתר). החלק של ברירת המחדל לא עובד מפני שמשום מה הכנסתי אותו בבלוק של התנאי שכן נמצאה תמונה… אבל אין לי זמן לתקן את זה כי יש ממש מעט פוסטים בלי תמונות, והם ישנים, אז זה לא נמצא בסדר עדיפות גבוה:

/**
 * Display the first image from the post, wherever you want
 * 'catch_that_image' from http://css-tricks.com/snippets/wordpress/get-the-first-image-from-a-post/
 * */
function kamoha_catch_that_image() {
 global $post, $posts;
 $first_img = '';
 ob_start();
 ob_end_clean();
 $output = preg_match_all( '/<img.+src=[\'"]([^\'"]+)[\'"].*>/i', $post->post_content, $matches );
 if ( count( $matches ) > 0 && count( $matches[0] ) > 0 ) {
    $first_img = $matches[1][0];
    // if no matches, return a default image
    if ( empty( $first_img ) ) {
        $first_img = content_url() . "/uploads/2014/05/Logo-big.png";
   }
 }
 return $first_img;
}

חזרה לפונקציה שלנו.
את כל קטע הקוד הבא עד הסוף הפונקציה, אני עוטפת בתנאי שנמצאה תמונה: if ( !empty( $imgsrc ) ). אמנם זה קצת מיותר משתי סיבות שמיד אפרט, אבל זה מה יש 🙂 זה באמת לא הקוד המוצלח ביותר שכתבתי מעודי.
אז למה התנאי הזה מיותר? ראשית מפני שמאחר שאני משֹימה תמונת ברירת מחדל, ברור שתמיד תימצא תמונה ב-$imgsrc ולכן התנאי תמיד יתקיים. שנית מפני שאין לי else… כלומר הקוד בכלל לא מתמודד עם מצב של העדר תמונה (כי כאמור לא אמור לקרות מצב כזה)! מי אמר חוסר הגיון ולא קיבל.

האם התמונה שמצאנו היא בגודל המקורי או ווריאציה מוקטנת?

השלב הבא אחרי שלכדנו את התמונה הראשונה בפוסט הוא לבדוק אם מה שמצאנו הוא התמונה המקורית בגודלה המלא או איזושהי ווריאציית-גודל שוורדפרס יצרה: כידוע, כשמעלים תמונה לוורדפרס, נוצרים העתקים של התמונה בגדלים שונים, לפי מה שמוגדר בממשק הניהול -> הגדרות -> מדיה, ובקוד (תבניות ו/או תוספים). כשמדובר בתמונה שנלכדת מתוך הפוסט, בד”כ מדובר באחת הווריאציות מאחר שהתמונה המקורית בד”כ גדולה מדי להופעה סתם ככה בפוסט. בשדה המיוחד זה גם ככה לפעמים, שמוכנסת הכתובת של אחת התמונות המוקטנות.
בבואי לשייך תמונה לפוסט, אני רוצה לשייך את התמונה המקורית בגודל המלא. כדי למצוא אותה כתבתי פונקציה בשם kamoha_get_img_src_without_size שאסביר מיד.
אז קודם אני מאתחלת את המשתנה שיכיל את ה-id של התמונה שמצאנו – נשתמש ב-id הזה מאוחר יותר כדי לשייך אותו לפוסט – ואחרי זה אני קוראת לפונקציה הזאת:

 $attachment_id = 0; /* the id of the image that will become the thumbnail */
 
 $orig_imgsrc = kamoha_get_img_src_without_size( $imgsrc ); // get the image without its size part of its name

הפונקציה kamoha_get_img_src_without_size מסתמכת על שיטת השיום (נתינת השמות) של וורדפרס לתמונות מוקטנות – הוספת ממדי התמונה לשם הקובץ, אחרי מקף. כך למשל, אם לקובץ המקורי קוראים example.jpg, אז לווריאציה שנוצרת לו בגודל 345 פיקסלים (רוחב) על 220 פיקסלים (גובה) יקראו example-345x220.jpg. לכן הבדיקה כוללת את מציאת המקף האחרון, וידוא שהאות x נמצאת בינו לבין הנקודה שלפני סיומת הקובץ, ו-וידוא שהמקף לא רחוק מדי מהנקודה (בחרתי לבדוק שהוא לא רחוק יותר מ-10 תווים משם. לא זוכרת מאיפה הגיע מספר הקסם 10). יכולתי לשכלל את הבדיקה ולבדוק ששתי המחרוזות – זו שלפני ה-x וזו שאחריה – הן מספרים, אבל לא עלה צורך בדיוק כזה. אז הנה הפונקציה:

/**
 * If an image src has its size as part of its name, remove the size.
 * This is needed in order to get to the orginal image
 * @param string $imgsrc
 * @return image url without size as part of name
 */
function kamoha_get_img_src_without_size( $imgsrc ) {
    $ret = $imgsrc;
    /* remove the size from end of filename */
    // look for last hyphen
    $last_hyphen = mb_strrpos( $imgsrc, '-' );
    /* make sure it's the right hyphen - 
     * if it's furhter than 10 characters away from the dot then it's not.
     * and the letter x has to appear there, because that's part of the syntax wordpress uses to add the sizes  */
    $last_dot = mb_strrpos( $imgsrc, '.' );
    $file_type = mb_substr( $imgsrc, $last_dot );
    if ( $last_dot - $last_hyphen > 10 || !mb_strpos( $imgsrc, 'x', $last_hyphen ) ) {
        $last_hyphen = mb_strlen( $imgsrc ) - mb_strlen( $file_type );
    }
    if ( $last_hyphen == -1 ) {
        $last_hyphen = null;
    }
    $ret = mb_substr( $imgsrc, 0, $last_hyphen ) . $file_type; //. '-' . $thumb_size 
    return $ret;
}

האם התמונה נמצאת באתר או שהיא כתובת של תמונה מאתר אחר?

אז יש לנו כתובת תמונה, ויש לנו – אולי – כתובת של התמונה המקורית. כעת צריך להתמודד עם מציאות שמתרחשת לעתים: לפעמים באמת נעשה שימוש בתמונה שהועלתה לאתר, אבל לפעמים הוכנסה תמונה או כתובת של תמונה שנמצאת מחוץ לאתר. לכן בפונקציה החמודה שלנו צריך לבדוק אם התמונה שמצאנו בשדה המיוחד, או לכדנו מתוך הטקסט, היא תמונה שכבר קיימת באתר ורק צריך לשייך אותה לפוסט, או שזו כתובת של תמונה מחוץ לאתר ואז צריך קודם להעלות אותה לאתר ורק אחרי זה לשייך אותה. לשם כך השתמשתי בפונקציה שמצאתי באחד התוספים של האתר – wordpress-popular-posts (הפונקציההיתה קיימת בו אז. כיום אני כבר לא מוצאת אותה) – שבודקת זאת באופן די פשוט:

/**
 * Check if url is local or external
 * @param type $url
 * @return true if external, false if local
 */
function is_external_image( $url ) {

    $dir = wp_upload_dir();

    // baseurl never has a trailing slash
    if ( false === strpos( $url, $dir['baseurl'] . '/' ) ) {
        // URL points to a place outside of upload directory
        return true;
    }

    return false;
}

אם היא מחוץ לאתר – העלאה ושיוך

אם באמת התמונה היא מחוץ לאתר, נעלה אותה בסיועה של פונקציה בשם handle_sideload_and_get_id. כמובן נבדוק קודם אם יש תמונה מקורית (במשתנה $orig_imgsrc) ואם כן נעלה אותה. אם לא – נעלה את התמונה שמצאנו בשדה הטקסט או בתוכן הפוסט וששמרנו במשתנה $imgsrc.

/* Sometimes a url of an external image is used in the user field, or in the post.
 * If that's the case, then first the image has to be uploaded as an attachment to the post */
 if ( is_external_image( $imgsrc ) ) {

    /* check if full size image exists */
    $response = wp_remote_get( $orig_imgsrc );
    if ( is_array( $response ) ) {
        $attachment_id = handle_sideload_and_get_id( $orig_imgsrc, $post_id, '' ); // try to get the id of the original image
    } else {
        $attachment_id = handle_sideload_and_get_id( $imgsrc, $post_id, '' ); // get the id of the image found in the user field or post
    }
 }

את הפונקציה handle_sideload_and_get_id מצאתי היכנשהו ברחבי הרשת אבל לצערי הרב לא תיעדתי מאיפה! הפונקציה הזאת משתמשת בפונקציות מוכנות של וורדפרס של העלאה ושיוך קבצים.

/**
 * Upload an image from a given url as a post thumbnail
 * @param type $url
 * @param type $post_id
 * @param type $description
 * @return type
 */
function handle_sideload_and_get_id( $url, $post_id, $description ) {

    require_once ( ABSPATH . "wp-admin" . '/includes/file.php' );
    require_once ( ABSPATH . "wp-admin" . '/includes/media.php' );
    require_once ( ABSPATH . "wp-admin" . '/includes/image.php' );
    $id = '';
    $file = download_url( $url );

    // Set variables for storage
    // fix file filename for query strings
    preg_match( '/[^\?]+\.(jpg|JPG|jpe|JPE|jpeg|JPEG|gif|GIF|png|PNG)/', $url, $matches );
    $file_array = array();
    if ( count( $matches ) > 0 ) {
        $file_array['name'] = basename( $matches[0] );
        $file_array['tmp_name'] = $file;

        // If error storing temporarily, unlink
        if ( is_wp_error( $file ) ) {
            @unlink( $file_array['tmp_name'] );
            $file_array['tmp_name'] = '';
        }

        // do the validation and storage stuff
        $id = media_handle_sideload( $file_array, $post_id, $description );
        // If error storing permanently, unlink
        if ( is_wp_error( $id ) ) {
            @unlink( $file_array['tmp_name'] );
        }

        set_post_thumbnail( $post_id, $id );
    }

    return $id;
}

זה סוף התנאי של תמונה שמחוץ לאתר.

אם התמונה היא באתר – מציאת ה-id שלה, ומה קורה אם לא נמצא

כעת מגיע תור ה-else, למצב שבו התמונה כבר נמצאת באתר.
במקרה כזה צריך רק למצוא איזה id הוא קיבל בתור attachment, ולשייך אותו לפוסט. שוב אנחנו נדרשים להחליט אם מדובר במשתנה $orig_imgsrc או ב-$imgsrc, ולהביא את ה-id בהתאם:

                /* If the image is alreay on the site, get its id. */
                $attachment_id = kamoha_get_attachment_id_from_url( $orig_imgsrc ); // try to get the id of the original image

                if ( !$attachment_id ) {
                    // if the id of the sizeless image doesn't exist, use the image that was found
                    $attachment_id = kamoha_get_attachment_id_from_url( $imgsrc );
                }

את הפונקציה שמוצאת id של קובץ לפי ה-url שלו, מצאתי בפוסט הזה:

/**
 * Get the ID of a WordPress image attachment from the image URL 
 * https://philipnewcomer.net/2012/11/get-the-attachment-id-from-an-image-url-in-wordpress/
 * @global type $wpdb
 * @param type $attachment_url
 * @return type
 */
function kamoha_get_attachment_id_from_url( $attachment_url = '' ) {

    global $wpdb;
    $attachment_id = false;

    // If there is no url, return.
    if ( '' == $attachment_url )
        return;

    // Get the upload directory paths
    $upload_dir_paths = wp_upload_dir();

    // Make sure the upload path base directory exists in the attachment URL, to verify that we're working with a media library image
    if ( false !== strpos( $attachment_url, $upload_dir_paths['baseurl'] ) ) {

        // If this is the URL of an auto-generated thumbnail, get the URL of the original image
        $attachment_url = preg_replace( '/-\d+x\d+(?=\.(jpg|jpeg|png|gif)$)/i', '', $attachment_url );

        // Remove the upload path base directory from the attachment URL
        $attachment_url = str_replace( $upload_dir_paths['baseurl'] . '/', '', $attachment_url );

        // Finally, run a custom database query to get the attachment ID from the modified attachment URL
        $attachment_id = $wpdb->get_var( $wpdb->prepare( "SELECT wposts.ID FROM $wpdb->posts wposts, $wpdb->postmeta wpostmeta WHERE wposts.ID = wpostmeta.post_id AND wpostmeta.meta_key = '_wp_attached_file' AND wpostmeta.meta_value = '%s' AND wposts.post_type = 'attachment'", $attachment_url ) );
    }

    return $attachment_id;
}

/**
 * Upload an image from a given url as a post thumbnail
 * @param type $url
 * @param type $post_id
 * @param type $description
 * @return type
 */
function handle_sideload_and_get_id( $url, $post_id, $description ) {

    require_once ( ABSPATH . "wp-admin" . '/includes/file.php' );
    require_once ( ABSPATH . "wp-admin" . '/includes/media.php' );
    require_once ( ABSPATH . "wp-admin" . '/includes/image.php' );
    $id = '';
    $file = download_url( $url );

    // Set variables for storage
    // fix file filename for query strings
    preg_match( '/[^\?]+\.(jpg|JPG|jpe|JPE|jpeg|JPEG|gif|GIF|png|PNG)/', $url, $matches );
    $file_array = array();
    if ( count( $matches ) > 0 ) {
        $file_array['name'] = basename( $matches[0] );
        $file_array['tmp_name'] = $file;

        // If error storing temporarily, unlink
        if ( is_wp_error( $file ) ) {
            @unlink( $file_array['tmp_name'] );
            $file_array['tmp_name'] = '';
        }

        // do the validation and storage stuff
        $id = media_handle_sideload( $file_array, $post_id, $description );
        // If error storing permanently, unlink
        if ( is_wp_error( $id ) ) {
            @unlink( $file_array['tmp_name'] );
        }

        set_post_thumbnail( $post_id, $id );
    }

    return $id;
}

כעת מגיע קטע קוד שאני ממש לא זוכרת אם הוא נכתב מפני שמשהו בכל זאת השתבש, או שהוא נועד לכסות מקרה שלא קיים מעשית: אם באף אחת מהמתודות האלה לא חוזר id, פשוט נעלה את התמונה שוב. לכאורה נראה שקטע הקוד הזה הוא מיותר, מפני שאם התמונה נמצאת באתר אז ודאי שחייב להיות לה id.

                if ( !$attachment_id ) {
                    /* check if full size image exists */
                    $response = wp_remote_get( $orig_imgsrc );
                    if ( is_array( $response ) ) {
                        $attachment_id = handle_sideload_and_get_id( $orig_imgsrc, $post_id, '' ); // try to get the id of the original image
                    } else {
                        $attachment_id = handle_sideload_and_get_id( $imgsrc, $post_id, '' ); // get the id of the image found in the user field or post
                    }
                }

שיוך סופי של התמונה והצגתה

אחרי כל הבדיקות אם זו תמונה חיצונית או מתוך האתר וטיפול בכל אחד מהמקרים, משייכים את התמונה לפוסט, ומשתמשים בפונקציית וורדפרס הרגילה של הצגת תמונה ראשית. לא ברור לי למה קטע הקוד הזה נמצא מחוץ לבדיקה של התמונה החיצונית, כי לכאורה בפונקציה שמעלה את התמונה יש גם שיוך לפוסט. אך מאחר שזה כך, ועבר זמן רב מאז שהתעסקתי עם הפונקציה הזאת ואין לי זמן לחזור אליה כרגע, אשאיר זאת כך (כן, יש לי legacy code מבאס):

            $success = add_post_meta( $post_id, '_thumbnail_id', $attachment_id );

            echo get_the_post_thumbnail( null, $thumb_size );

וזה הסוף. את הקוד במלואו אפשר לראות בגיטהאב, ולהשתמש בו ע”י קריאה ל-kamoha_show_homepage_thumbnail עם פרמטר של גודל ה-thumbnail  המבוקש – ממש כמו שקוראים לפונקציות get_the_post_thumbnail או the_post_thumbnail.

שונות

הסתייגות אחת: ברבות הימים ומסיבות שונות, נזקקתי לתוסף שמאפשר להעלות עוד תמונה ראשית לכל פוסט. הייתי מוכרחה להתייחס לתוסף הזה בפונקציה הזאת, וזה קורה מיד בתחילתה. אם אין לכם התוסף הזה – תכלס רוב הסיכויים הם שאין לכם – שורות 22-41 מיותרות אז פשוט תמחקו אותן, ואת הסוגר המסולסל בשורה 43.

אם יש לכם שאלות, הערות, תיקונים או תובנות – אשמח עד מאוד אם תשתפו אותם איתי בכל דרך שתחפצו – פרטית או ציבורית.

נ”ב: הפרק הזה הוא פרק ביניים ומכאן מספרו הבלתי- שלם. ספוילר קטן: בשלושת השבועות הבאים מתוכננים 3 פרקי ביניים, והם קיבלו את המספרים 12 ורבע, 12 וחצי ו-12 ושלושת רבעי. הפוסט הזה כאמור הוא תוספת ספונטנית, ומכיוון שלא רציתי לשנות את מספרי הפרקים הנ”ל (גם מתוך עצלות וגם מפני שהפרק הנוכחי לא מתפרסם בכמוך) נתתי לו מספר בין 12 ל-12.25. אמנם מצד אחד זה לא מאוזן, אבל מצד שני הוא משלים לי את פרקי הביניים לארבעה, וזה משתלב מעולה עם החלוקה לארבעה שסיפרתי עליה בפרק שעבר.

כ

כתבו תגובה

כתובת הדוא"ל שלכם לא תוצג.