RatingControl

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
redhead
Člen | 1313
+
0
-

EDIT: Plugin se nyní nalézá v extras


STARÝ POST:

Zdravím,

rád bych se chtěl ujistit, že tento návrh komponenty (i když malé) není špatný, co se týče implementace. Pokud ne, tak bych to dal do extras. Je to spíš proto abych měl jasno v tvoření komponent.

Jedná se o RatingControl, tedy jednoduchou komponentu, která zobrazí nějaké ty hvězdičky třebas pod článkem, nebo produktem, nebo v nějaké tabulce, s možností hlasovat o hodnocení:

RatingModel interface

Pro získání informací pro RatingControl (aktuální rating, počet hlasů, bylo hlasováno, …) a pro vložení nového ratingu:

$ratable může být cokoliv (předává se v šabloně, viz dále), z čeho programátor dostane ty informace, např. ID článku nebo samotný objekt článku:

$id je unikátní ID pro ten control (získaný přes getUniqueId)

interface IRatingModel
{
    public function wasVoted($id);
    public function getUniqueId($ratable);
    public function getRating($ratable);
    public function getVoteCount($ratable);
    public function addRating($id, $newRating);
}

Třída

Samotná třída je jednoduchá, pouze musíme nastavit model implementující IRatingControl. Teď koukám, že createTemplate by se měla ještě změnit (nemít tam ten hardcoded soubor s šablonou)

/**
 * RatingControl
 *
 * @author Radek Ježdík
 */
class RatingControl extends Control
{
    /**
     * Model interface instance for getting rating information
     * @var IRatingModel
     */
    private $model = null;

    /**
     * Number of stars, default 5 stars
     * @var int
     */
    private $total = 5;

    /**
     * Sets Rating Model, to get the information from
     * @param IRatingModel implementation
     */
    public function setRatingModel($model) {
        $this->model = $model;
    }

    /**
     * Set total number of stars
     * @param int
     */
    public function setTotal($stars) {
        $this->total = $stars;
    }

    /**
     * Rate signal, user voted
     * @param int unique ID of rating control
     * @param int new rating specified by user
     */
    public function handleRate($id, $newRating) {
        if($this->model->wasVoted($id))
            return;

        $newRating = max(1, (int)$newRating);
        $newRating = min($newRating, $this->total);

        $this->model->addRating($id, $newRating);
        $this->invalidateControl();
    }

    public function createTemplate() {
        $template = parent::createTemplate();
        $template->setFile(APP_DIR.'/controls/ratingControl.phtml');

        return $template;
    }

    /**
     * Renders the rating control
     *
     * @param mixed is passed to Rating Model methods
     */
    public function render($ratable)
    {
        if($this->model != null) {
            $this->template->total = $this->total;

            $this->template->id = $this->model->getUniqueId($ratable);
            $this->template->rating = round((float)$this->model->getRating($ratable));
            $this->template->voteCount = (int)$this->model->getVoteCount($ratable);
            $this->template->voted = $this->model->wasVoted($this->template->id);

            $stars = array();
            for($i=1; $i <= $this->total; $i++)
                $stars[$i] = (bool)($i <= $this->template->rating);
            $this->template->stars = $stars;

            $this->template->render();
        }
    }
}

Příklad šablony

{snippet $id}
	{foreach $stars as $i => $st}
        	{if !$voted}<a href="{link rate $id, $i}" class="ajax">{/if}
        		<button {attr class('rate-star') class('star-full', $st)}></button>
            	{if !$voted}</a>{/if}
       	{/foreach}
       	{if $voted}<small>thank you for voting!</small>{/if}
       	<div><small>({$voteCount} votes)</small></div>
{/snippet}

Příklad použití

implementace modelu, nechtělo se mi psát těla metod, neboť skoro všechny jen šahají do databáze.

class RatingControlModel implements IRatingModel
{
    public function wasVoted($id){
        //vrací bool, zda bylo tímto uživatelem hlasováno
    }
    public function getUniqueId($ratable) {
        //vrací unikátní ID, třeba ID produktu
    }
    public function getRating($ratable) {
        //vrací aktuální rating pro $ratable
    }
    public function getVoteCount($ratable) {
        //vrací int s počtem hlasování pro $ratable
    }
    public function addRating($id, $newRating) {
        //bylo hlasováno, uložení do session, do databáze, cokoliv...
	//vložení nového hlasování do databáze
    }
}

továrnička v presenteru:

	public function createComponentRating($name)
	{
	$rating = new RatingControl($this, $name);
	$rating->setRatingModel(new RatingControlModel());
		return $rating;
	}

šablona:

@{control rating $product}

nebo třeba pro nějaký výpis:

{foreach $products as $prod}
	@{control rating $prod}
{/foreach}

$product, resp. $prod v posledních dvou šablonách, může být objekt produktu, ten se pak předává v modelu jako $ratable (může to taky být třeba ID toho produktu {control rating $prod->id} ).

Doufám, že takovýto návrh použití stejného objektu komponenty, pro více různých použití, není špatným návrhem.

Díky za jakékoliv návrhy, připomínky.

Editoval redhead (4. 8. 2010 13:59)

Ondřej Brejla
Člen | 746
+
0
-

Mám dojem, že stejnou funkčnost mohu implementovat pomocí rozšíření PollControlu, stejně jako je tomu u dvou příkladů (tedy anketa s odkazy a formem). Ale nijak jsem tvůj kód nezkoumal, odhaduji jen podle popisu a názvu :-)

Honza Kuchař
Člen | 1662
+
0
-

Malinkatý detail: Když už používáš gettery/settery nebylo by špatné, aby výjimka vyletěla hned při nastavování parametru. (lépe se to pak hledá)

/**
 * Sets Rating Model, to get the information from
 * @param IRatingModel implementation
 */
public function setRatingModel(IRatingModel $model) { // Tady jsem přidal IRatingModel
    $this->model = $model;
}

Jinak já myslím, že implementace vypadá dobře. ;) Klidně bych to dal do extras.

Editoval honzakuchar (4. 10. 2009 12:56)

Honza Marek
Člen | 1664
+
0
-

Tak já dám několik nápadů s různou mírou relevance. Není to kritika, ale spíše věci k zamyšlení.

  • Líbí se mi, že je komponenta jednoduchá a způsob jakým zachází s modelem.
  • Pevné nastavení šablony tímhle způsobem je opravdu nešikovné. Většinou šablony u Controlů bývají ve stejné složce jako Control samotný a cesta k nim se dá nastavit jako dirname(__FILE__) . '/RatingControl.phtml nebo dirname(__FILE__) . '/template.phtml.
  • Kvůli nastavení souboru šablony nemusíš přepisovat createTemplate, stačí zavolat $this->template->setFile(...) v metodě render.
  • Název interface modelu by podle mě stačil IRatingModel.
  • Šabloně by se nemuselo předávat takových parametrů, kdyby se jí předal třeba jen model.
  • Před dodáním do extras bych důkladně nastudoval coding standards nette. Já taky normálně třeba neodsazuju složenou závorku za hlavičkou funkce na nový řádek, ale když je to něco do Nette\Extras, tak to udělám apod.
  • Moc nechápu, co má být $ratable a v modelu metoda getUniqueId.

Možná by se dal ten model zjednodušit.

Interface:

interface IRatingModel
{
    public function wasVoted();
    public function getRating();
    public function getVoteCount();
    public function addRating($value);
}

Implementace by pak byla volnější, třeba:

class ArticleRatingModel extends Object implements IRatingModel
{
	private $id;
	private $user;

	public function __construct($id, $user)
	{
		// nastavím id a uživatele
	}

	// zbytek metod je podobných, ale nemají parametry, id a uživatele si pamatuje model
}
redhead
Člen | 1313
+
0
-
  • createTemplate jsem přepsal kvůli dědičnosti, kterou jsem na localhostu využil v případě, kde jsem měl widget rating (v zobrazení jednoho produktu) a smallRating (pro zobrazení výpisu produktů, kde to bylo menší, a nebylo možno hlasovat), takže jsem pro SmallRatingControl podědil RatingControl, a změnil cestu k upravené šabloně přetížením createTemplate. Ale podívám se ještě na to.
  • název interface, jsem už změnil před tím než jsi to napsal, měl jsem tam totiž jakousi nekonzistenci v IRatingControlModel a IRatingModel (copy n paste problém :) )
  • šabloně bych model předat mohl, ale pak by se tam objevovali nevzhledná volání těch metod, a to by se mi moc nelíbilo (třebas $voteCount X $model->getVoteCount() ). Ještě uvidím, až to budu dolaďovat
  • coding standards dodržuju běžně (osobně se mi nelibí závorka na konci definice), ale tady jsem to udělal proto, aby to nebylo tak zbytečně dlouhý.. :)
  • getUniqueID využívám pro jednoznačné rozlišení více ratingControl na stránce pro více objektů (článku, produktů, ..) aby se nerefreshovali všechny, ale jen ten na který jsem kliknul.
  • a co se týče toho modelu, nevím nevím, zda-li by to pak fungovalo, to bych musel vytvářet několik těch instancí controlu a u každého nastavovat model s user a id, možná to není očividné, ale tahle komponenta, i když je využívána několikrát na stránce, tak vždycky je jenom jedna instance, mění se jen ID, který se právě předává metodě render, aby se těch několik komponent od sebe rozlišilo. Ale možná sem teď nepochopil co myslíš ty :)

nebylo by špatné, aby výjimka vyletěla hned při nastavování parametru

jj, už to tam mám. Díky :)

Editoval redhead (4. 10. 2009 14:04)

redhead
Člen | 1313
+
0
-

ještě ten $ratable jsem zapomněl, je to proměnná mixed, která se předává render metodě, tedy ve volání

{control rating $product}

z tohoto objektu/id pak můžu v modelu, zjistit informace pro tento objekt/id. Pokud je to objekt (třeba product), tak může mít ty informace už přímo v sobě, nebo je třeba získá přes $ratable->id z tabulky. Pokud je to nějaké id produktu tak to samý, vybere to z tabulky.

Editoval redhead (4. 10. 2009 14:14)

Honza Marek
Člen | 1664
+
0
-

redhead napsal(a):

  • createTemplate jsem přepsal kvůli dědičnosti, kterou jsem na localhostu využil v případě, kde jsem měl widget rating (v zobrazení jednoho produktu) a smallRating (pro zobrazení výpisu produktů, kde to bylo menší, a nebylo možno hlasovat), takže jsem pro SmallRatingControl podědil RatingControl, a změnil cestu k upravené šabloně přetížením createTemplate. Ale podívám se ještě na to.

Tohle by elegantně vyřešila metoda setTemplate.

  • šabloně bych model předat mohl, ale pak by se tam objevovali nevzhledná volání těch metod, a to by se mi moc nelíbilo (třebas $voteCount X $model->getVoteCount() ). Ještě uvidím, až to budu dolaďovat

Pokud model bude potomek Nette\Object, tak stačí $model->voteCount, což není nijak hrozné.

  • getUniqueID využívám pro jednoznačné rozlišení více ratingControl na stránce pro více objektů (článku, produktů, ..) aby se nerefreshovali všechny, ale jen ten na který jsem kliknul.

Aha, rozumim.

  • a co se týče toho modelu, nevím nevím, zda-li by to pak fungovalo, to bych musel vytvářet několik těch instancí controlu a u každého nastavovat model s user a id, možná to není očividné, ale tahle komponenta, i když je využívána několikrát na stránce, tak vždycky je jenom jedna instance, mění se jen ID, který se právě předává metodě render, aby se těch několik komponent od sebe rozlišilo. Ale možná sem teď nepochopil co myslíš ty :)

Tohle jsou taky rozumný argumenty :)

Honza Marek
Člen | 1664
+
0
-

A zase jsem na to zapomněl… :D

Umí ta komponenta zobrazovat výsledky jako vybarvené hvězdičky a půlhvězdičky a aby při hlasování po najetí myši ukázala vybarvený počet hvězdiček jako třeba na youtube?

redhead
Člen | 1313
+
0
-

Honza M. napsal(a):

Tohle by elegantně vyřešila metoda setTemplate.

ha! Nepřepsal by se ale potom ten soubor který bych předal v setTemplate, kdybych dal $this->template->setFile() volání do render fáze??

Jinak, je to stále dobrý návrh? Mít stejnou instanci, kterou využiji několikrát na stránce s jinými parametry?

redhead
Člen | 1313
+
0
-

Honza M. napsal(a):

Umí ta komponenta zobrazovat výsledky jako vybarvené hvězdičky a půlhvězdičky a aby při hlasování po najetí myši ukázala vybarvený počet hvězdiček jako třeba na youtube?

No, to už je starost JavaScriptu, ovšem nezahálel jsem a v Prototypu jsem si vytvořil na to jednoduchou funkci, která toto zajišťuje krásně. Věřím že v jQuery to nebude problémem :D . Řekl bych že by se dalo využít i nějakého toho jQuery pluginu co jsem viděl. Ovšem nezkoumal jsem to do podrobna. Ale mám to v TODO listu :D

Jinak půl hvězdičky to neumí (zatím). Nejdřív udělám tuto verzi, a v další se podívám na půlhvězdičky :D

Honza Marek
Člen | 1664
+
0
-

redhead napsal(a):

Honza M. napsal(a):

Tohle by elegantně vyřešila metoda setTemplate.

ha! Nepřepsal by se ale potom ten soubor který bych předal v setTemplate, kdybych dal $this->template->setFile() volání do render fáze??

:) To by se tam dal třeba nějaký if. Anebo můžeš zůstat u přepsané metody.

Jinak, je to stále dobrý návrh? Mít stejnou instanci, kterou využiji několikrát na stránce s jinými parametry?

Nemusí to být stejná instance. Přijde mi jednoduší mít dvě továrničky, kde v jedné přenastavím komponentě šablonu než mít dvě továrničky a jednu třídu navíc…

JakubKohout
Člen | 92
+
0
-

Určitě zařadit do extras. Je to dobře napsaný (podle mě) až teda na ten template ale neni problém si to poupravit že :o)

Honza Kuchař
Člen | 1662
+
0
-

Nechcete to přidat do extras?