Chmura tagów w Zend Framework

by Mateusz Tymek — on Zend Framework, PHP

Head's up! This post was written back in 2009 and is very likely to contain outdated information.

Wraz z pojawieniem się wersji 1.8 frameworka jego autorzy udostępnili nowy komponent: chmurę tagów (Zend_Tag_Cloud). Po bliższym zapoznaniu się z dokumentacją okazało się że jej możliwości ograniczone są do wyświetlania chmury na podstawie określonych dekoratorów. Programista sam musi połączyć ten komponent z modelem danych. Postaram się zaprezentować sposób jaki wykorzystałem w swoim blogu.

Chmury tagów

Chmury tagów (znaczników) w graficzny sposób prezentują słowa kluczowe opisujące zawartość witryny. Każde z nich ma przypisaną wagę, czyli liczbę oznaczającą istotność danego słowa. Im większa waga tym dane słowo powinno bardziej się wyróżniać w chmurze (na przykład większą czcionką czy zmienionym kolorem).
W systemach CMS (typu Joomla) przy każdym artykule można w jawny sposób oznaczyć słowami kluczowymi. Waga danego słowa jest równoważna liczbie artykułów nim oznaczonych. Taki właśnie taki model postanowiłem zastosować w silniku tej strony - efekt jest widoczny w lewej kolumnie tego blogu.

więcej o chmurach tagów: http://en.wikipedia.org/wiki/Tag_cloud

Baza danych

Czas przejść do programowania. Pierwszą czynnością będzie utworzenie odpowiednich tabel w bazie danych. Potrzebne nam będą dwie tabele - pierwsza z nazwami tagów, oraz druga - przechowująca ich relacje z treścią strony.

CREATE TABLE tag (
    id    INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(40)
);

CREATE TABLE tag_relationship (
    tag_id     INT NOT NULL,
    article_id INT NOT NULL,
    PRIMARY KEY (tag_id, article_id)
);

 W drugiej tabeli pole article_id oznacza identyfikator artykułu powiązanego z danym tagiem.

Można na tym poprzestać, jednak proponuję jeszcze utworzyć prosty widok który przyporządkuje każdemu znacznikowi jego wagę i wyświetli je w kolejności alfabetycznej:

CREATE VIEW tag_weights AS
SELECT tag.id, tag.title, COUNT(*) AS weight
FROM tag, tag_relationship
WHERE tag.id = tag_relationship.tag_id
GROUP BY tag.id
ORDER BY tag.title ASC;

Model

Mając utworzoną strukturę tabel możemy zabrać się za przygotowanie modelu. W tym przypadku model będzie klasą dziedziczącą z klasy Zend_Tag_Cloud. Takie rozwiązanie jest bardzo praktyczne - dzięki niemu nasza klasa będzie stosunkowo prosta do napisania, i jednocześnie będzie potrafiła wyświetlić chmurę.

Zanim zaczniemy analizować kod należy przyjąć dwa założenia: Zend_Registry będzie zawierać adapter bazy danych pod nazwą "db", a klasa Model_DbTable_Tag będzie pochodną Zend_Db_Table operującą na tablicy "tag".

Pierwszym krokiem będzie napsianie konstruktora który wczyta listę znaczników z bazy danych. Ponieważ znaczniki są identyfikowane poprzez pole ID, napiszemy też funkcję zamieniającą ich nazwy na liczbowy identyfikator. Funkcja ta automatycznie utworzy znacznik jeśli nie ma go jeszcze w bazie.

class Model_TagCloud extends Zend_Tag_Cloud
{
    protected $_tagsByTitle;

    public function __construct()
    {
        $db = Zend_Registry::get('db');

        $tags = $db->fetchAll($db->select()->from('tag_weights'));

        foreach ($tags as $tag) {
            $this->appendTag(array(
                'title' => $tag['title'],
                'weight' => $tag['weight'],
                'params' => array(
                    'url' => '/content/tag/tag/'.$tag['title']
                )
            ));
            $this->_tagsByTitle[mb_strtolower($tag['title'])] = $tag['id'];
        }
    }

    public function getTagId($title)
    {
        if (!array_key_exists($title, $this->_tagsByTitle)) {
            $table = new Model_DbTable_Tag();
            $id = $table->insert(array('title' => $title));
            $this->_tagsByTitle[$title] = $id;
        }

        return $this->_tagsByTitle[$title];
    }
}

Konstruktor wczytuje wszystkie tagi (wraz z ich wagami) poprzez utworzony wcześniej widok. Następnie znaczniki dodawane są do chmury przy pomocy metody appendTag(). Zmienna $_tagsByTitle przechowuje znaczniki w formie tablicy której klucze są nazwami znaczników a wartości - ich identyfikatorami. Umożliwi to łatwe sprawdzenie czy dany znacznik już istnieje. Zajmuje się tym metoda getTagId().

Teraz kolej na funkcję która zapisze znaczniki przyporządkowane do danej treści. Jej parametrami będzie identyfikator artykułu oraz tablica znaczników.

public function saveTags($articleId, $tags)
    {
        $db = Zend_Registry::get('db');

        $tagValues = array();
        foreach ($tags as $tag) {
            $tagValues[] = '('.$this->getTagId($tag).', '.$articleId.')';
        }
        $query = 'INSERT INTO tag_relationship (tag_id, article_id) VALUES'
               . implode(', ', $tagValues);
        $db->query($query);
    }

Funkcja ta kolejno wykorzystuje metodę getTagId() w celu znalezienia identyfikatora a następnie zapisuje daną relację. Przykładowo moglibyśmy wykorzystać ją w następujący sposób:

if ($article->isValid()) {
    $article->save();
    $tags = array('znacznik1', 'znacznik2', 'inny znacznik');
    $tagCloud->saveTags($article->getId(), $tags);
}

Widok

Ostatnią czynnością jaka pozostała jest wyświetlenie chmury. Jest to zaskakująco proste - wystarczy umieścić w pliku widoku taki kod:

$tagCloud = new Model_TagCloud();
echo $tagCloud;

Chmura zostanie zbudowana przy użyciu listy nieuporządkowanej (czyli znaczników ul i li). Nie zawsze będzie to optymalne rozwiązanie - ja wolałem zastosować znaczniki <span> zagnieżdżone w bloku <div>. W tym celu trzeba zmienić domyślne dekoratory, co nie jest zbyt intuicyjne (i podczas pisania tekstu nie było dobrze opisane w manualu):

$tagCloud->setCloudDecorator(array(
        'decorator' => 'HtmlCloud',
        'options' => array(
            'htmlTags' => array('div' => array('class' => 'tag-cloud')),
        )
));
$tagCloud->setTagDecorator(array(
        'decorator' => 'HtmlTag',
        'options' => array(
            'htmlTags' => array('span'),

        )
));

Co dalej?

W ten sposób mamy już prostą klasę zarządzającą znacznikami i generującą chmurę, którą łatwo można dostosować do modelu własnej aplikacji. Pierwszym krokiem który warto wykonać dalej jest czyszczenie tagów i ich relacji. Wyobraźmy sobie sytuację gdy edytujemy artykuł i usuniemy część tagów. Metoda saveTags() powinna wziąć pod uwagę taką możliwość i nie tylko dodać nowe tagi, ale też usunąć te niepotrzebne. Najprościej dodać na początku tej funkcji kwerendę:

DELETE FROM tag_relationship WHERE article_id=$articleId

Jej wykonanie spowoduje usunięcie wszystkich relacji z danym artykułem.

Czasem może zajść potrzeba wyczyszczenia listy tagów z tych które nie mają już żadnego powiązania. Kwerenda:

DELETE FROM tag WHERE (SELECT COUNT(*) FROM tag_relationship
WHERE tag.id=tag_relationship.tag_id)=0

Osobna sprawa to powiązanie chmury z modelem danych. Możemy w jednym zapytaniu SQL wyciągać sam artykuł, a w drugim powiązane dane. Jednak w przypadku tego bloga bardziej opłacało się utworzyć widok łączący artykuł z tagami, autorem i komentarzami. Jako ciekawostkę prezentuję jedną z wersji zapytania (nie obejmuje jeszcze komentarzy):

SELECT content. * ,
    user.name AS author_name,
    user.fullname AS author_fullname,
    category.name AS category_name,
    GROUP_CONCAT( DISTINCT tag.title ORDER BY tag.title ASC SEPARATOR ', ' ) AS tags
FROM user, category, content
LEFT JOIN tag_relationship ON tag_relationship.content_id = content.id
LEFT JOIN tag ON tag.id = tag_relationship.tag_id
WHERE content.author = user.id
AND content.category = category.id
GROUP BY content.id;

To zapytanie jest już lekko skomplikowane jednak wciąż wykonuje się szybciej niż trzy osobne zapytania (artyuł, powiązany autor i powiązane tagi).

Przydatne linki

Więcej na ten temat:


comments powered by Disqus