Mapa stránek

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

Zdravím, jednou v noci co jsem vstal ve 4 ráno z důvodu vybírání zbytků v ledničče jsem se rozhodl, že budu generovat mapu stránek nějakou „hezčí“ cestou…

Co jsem si to tedy vymyslel:

@meta v phpDoc bude obsahovat nějaké užitečné info použité i pro template

/**
  @index
  @meta(name="title",content="Žraní koblih")
  @meta(name="keywords",content="la,la,la")
  @meta(name="description",content="lalala.")
  @meta(name="robots",content="index,follow")
 */

nebo také třeba v případě více stránek ->

/**
  @index
  @meta(PagesModel::getSitemap)
 */

Informace v komentářích se budou dědit: class → action → render

Co dále?

* Jak teď řešit nadřazené stránky… @parent()?
* Jak bude vypadat úložiště pole mapy ze kterého se mapa bude následně renderovat?
* Z těchto informací lze vlastně generovat i drobečkovou navigaci, jak to spáchat nějak inteligentně aniž by to příliš zatěžovalo běh?

Nějak jsem zkoušel něco sesmolit, ale možná bude nejdříve lepší dát dohromady nějaké další nápady a z toho to tedy nakonec napsat něco do extras neb tohle se určitě hodí více lidem.

Editoval amsys (28. 9. 2009 12:33)

redhead
Člen | 1313
+
0
-

taky už jsem nad tím nejednou uvažoval. Drobečkovou komponentu jsem si dokonce napsal, ale zázrak to nebyl, rozhodně to nebylo moc friendly:

$this->breadcrumb->add('Hlavní stránka', 'Default:default');
$this->breadcrumb->add('Kategorie', 'Default:category');
$this->breadcrumb->add($product->name, 'Product:default', $product->id);
//což vygenerovalo odkazy oddělené např. '>'

S anotace jsem si také v hlavě pohrával, ale k nějaké implementaci jsem se nedokopal.

Editoval redhead (28. 9. 2009 10:48)

amsys
Člen | 20
+
0
-

Template budou předávany objekty meta a breadcrumbs:

Object $meta {
	title (string)
	robots (string)
	keywords (string)
	description (string)
}
Object $breadcrumbs {
	Object { $item ->
		link (string)
		name (string)
		title (string)
	}
	Object $item -> ...
}

a kde tyto 2 objekty získáme? Zde mě napadá využití afterRender, ověříme si zda v render{view} byl nastaven dynamický obsah, pokud ne vytáhneme z cache. V případě dynamické stránky je třeba doplnit chybějící stránky – breadcrumbs nebudou kompletní tz. v metodě afterRender doplníme až do konce. S pomocí RobotLoaderu by načítání nadřazených presenterů pro získání Reflection neměl být problém.

Pro tento případ aktualizace stránek mě nepadá v případě aktualizace „části“ databáze se provede touch() na nějakej soubor který použijeme jako podmínku znovunačtení této části cache.

Limitace: Statický obsah nemůže odkazovat na Dynamickou parent stránku. Zde může dojít k chybě.

V případě generování SiteMap, jak zjistíme použitelné presentery? Zkoušel jsem cache: Nette.RobotLoader->list což asi nebude nejlepší řešení.

Metody pro získání meta informací a breadcrumbs můžeme využít pro generování mapy.

Jak bude vypadat array pro sitemap, jak vyřešíme nadřazené stránky?

array
(
	':title' => 'a',
	':neco' => 'b',
	':children' => array(
		array(...),
		array(...),
	),
);

Nebo raději přes objekty?

ic
Člen | 430
+
0
-

Mluvíme tady o sitemap.xml souboru nebo o takové té stránce s kopou odkazů ?

amsys
Člen | 20
+
0
-

ic napsal(a):

Mluvíme tady o sitemap.xml souboru nebo o takové té stránce s kopou odkazů ?

Momentálně o smyšleném backendu který bude obojí schopen vyplivnout :-)

_Martin_
Generous Backer | 679
+
0
-

A je požadavkem toho backendu, že tuhle mapu stránek jenom vyplivne a nebo umožní uživateli i její změnu?

amsys
Člen | 20
+
0
-

_Martin_ napsal(a):

A je požadavkem toho backendu, že tuhle mapu stránek jenom vyplivne a nebo umožní uživateli i její změnu?

Takhle bych to zase nekomplikoval, ještě není pořádně vymyšlené to hlavní :-) tz. vyplivne (backendem jsem zde myslel model)

Vidím to takhle:

Část 1:
SitemapModel
* Zpracuje phpDoc informace ze všech presenterů, zavolá definované funkce pro získání dat indexovaného dynamického obsahu a vyhodí pole s mapou stránky
SitemapPresenter
* Zpracuje data z modelu do XML pro vyhledávače a do nějakého pěkného stromu xhtml

Část 2:
BasePresenter
* Obsahuje hook před vykreslením který doplní $meta a $breadcrumbs pomocí metody pomocné třídy ze SitemapModel která zpracuje informace ze současného Presenteru
* Asi by to mělo dávat pozor na případnou změnu v případě changeView

amsys
Člen | 20
+
0
-

Je to děs, šlape to a bude to ještě chtít ještě dost úprav. Any comments? Prosím nekamenovat, teprve v tom začínám :-)

Jo a psal jsem to nad 0.8 (tu aplikaci kde jsem to nasadil musím konečně převést, neb z 0.9 používám např. Mail)

Tímhle sestavíme strom:

<?php
class SiteNode extends Container {

    protected $parentName = FALSE;

    private $presenter = NULL;
    protected $params = array(
        'action' => NULL
    );

    // this must be replaced by something more unversal (more useful info in phpdoc)
    protected $index = FALSE;
    protected $title = '';
    protected $header = NULL;
    protected $meta = array();

    public function __construct($presenter, $action, $id = NULL)
    {
        $this->presenter = $presenter;
        $this->params['action'] = $action;
        parent::__construct(
            $presenter .
            self::NAME_SEPARATOR .
            $action .
            (!empty($id) ? self::NAME_SEPARATOR . $id : '')
        );
    }

    /**
     * Comparsion callback of method reflections for usort()
     * @param ReflectionMethod $a
     * @param ReflectionMethod $b
     * @return bool Comparsion result
     * @see function processReflections()
     */
    private static function sortMethods($a, $b) {

        // very simple: action < prepare < render (we don't care about rest)
        return ($a->name > $b->name);

    }

    /**
     *
     * @param ReflectionClass $presenter Presenter reflection
     * @param array $methods Array of ReflectionMethod
     * @return SiteNode Provides fluent interface
     */
    public function processReflections($presenter, $methods = array()) {

        $this->processReflection($presenter);

        // sort methods (this is not so necessary, but we use cache anyway)
        usort($methods, array(__CLASS__, 'sortMethods'));

        foreach($methods as $method) {
            $this->processReflection($method);
        }

        return $this;
    }

    private function processReflection($reflection) {

        $index = Annotations::get($reflection, 'index');
        if ($index != NULL) {
            $this->index = (bool) $index;
        }

        $title = Annotations::get($reflection, 'title');
        $this->title = (empty($title)) ? 'Undefined' : $title;

        $header = Annotations::get($reflection, 'header');
        if ($header !== FALSE) {
            $this->header = (empty($header)) ? $title : $header;
        }

        foreach(Annotations::getAll($reflection, 'meta') as $meta) {
            $this->meta[] = $meta;
        }

        $parent = Annotations::get($reflection, 'parent');
        if ($parent) {
            $this->parentName = $parent;
        }

    }

    public function setInformation($index, $title, $meta) {
        $this->setIndex($index);
        $this->setTitle($title);
        $this->setMeta($meta);
        return $this;
    }

    public function setIndex($index) {
        $this->index = ($index) ? TRUE : FALSE;
    }

    public function getIndex() {
        return $this->index;
    }

    public function setParentName($name, $relative = FALSE) {
        if ($name != NULL) {
            $this->parentName = ($relative) ?
                $this->presenter .
                self::NAME_SEPARATOR . $this->params['action'] .
                self::NAME_SEPARATOR . $name : $name;
        }
    }

    public function getParentName() {
        return $this->parentName;
    }

    public function setParams($params) {
        $this->params = isset($params['action']) ? $params : array_merge($this->params, $params);
        return $this;
    }

    public function getParams() {
        return $this->params;
    }

    public function setMeta($meta) {
        $this->meta = $meta;
        return $this;
    }

    public function getMeta() {
        return $this->meta;
    }

    public function setTitle($title) {
        if (!is_array($title)) {
            $this->title = $title;
        }
        return $this;
    }

    public function getTitle() {
        return $this->title;
    }

    public function getHeader() {
        return $this->header;
    }

    public function setHeader($header) {
        $this->header = $header;
        return $this;
    }

    public function getPresenter() {
        return $this->presenter;
    }

}

class Site extends Container {

    private $name = 'Site';

    protected $orphans = array();

    protected $files = array();

    public function __construct() {

        /** @var $presenter ReflectionClass */
        foreach($this->getPresenters() as $presenter) {

            /** @see SitemapModel::__construct() */
            $this->files[] = $presenter->getFilename();

            $nodes = $this->examinePresenter($presenter);

            foreach ($nodes as $node) {
                if ($node instanceof SiteNode) {
                    $this->attach($node);
                }
            }
        }

    }

    public function attach(SiteNode $node) {
        $name = $node->getParentName();
        if ($name) {
            //$parent = $this->getComponent($node->parentName, FALSE); // not going to work (is not recursive)
            //$parent = $this->getObject($node->parentName);
            $parent = $this->findObject($name);
            if (!$parent) {
                $this->addOrphan(
                    $name,
                    $node
                );
            } else {
                //$parent->addComponent($node, NULL);
                $parent->addObject($node);
            }
        }
        else
        {
            //$this->addComponent($node, NULL);
            $this->addObject($node);
        }
        $this->findOrphans($node);
    }

    protected function addOrphan($parent, $node) {
        if (!isset($this->orphans[ $parent ]))
            $this->orphans[ $parent ] = array();
        $this->orphans[ $parent ][] = $node;
    }

    protected function findOrphans(SiteNode $node) {
        if (isset($this->orphans[$node->name])) {
            foreach ($this->orphans[$node->name] as $orphan) {
                //$node->addComponent($orphan, NULL);
                $node->addObject($orphan);
            }
            unset($this->orphans[$node->name]);
        }
    }

    /**
     * Method which retrieves all loadable presenter reflections
     * @internal We use RobotLoader cache to get all presenters, any better way?
     * @return array Array of ReflectionClass
     */
    private function getPresenters() {

        $presenters = array();

        $cache = Environment::getCache('Nette.RobotLoader');

        if (isset($cache['data'])) {
            foreach(array_keys($cache['data']['list']) as $class) {
                if(preg_match('/presenter$/i', $class)) {
                    if (class_exists($class)) {
                        $r = new ReflectionClass($class);
                        if ($r->implementsInterface('IPresenter') && !$r->isAbstract()) {
                            $presenters[$class] = $r;
                        }
                    }
                }
            }
        }

        return $presenters;
    }

    /**
     *
     * @param <type> $reflection
     * @return <type>
     */
    private function examinePresenter(ReflectionClass $pref) {

        $methods = $pref->getMethods();
        $views = array();

        $callback = Annotations::get($pref, 'callback');

        if ($callback) {
            // one callback rules them all
            return call_user_func($callback, $this);
        }
        else
        {

            foreach ($methods as $method) {
                if(preg_match(
                    '/^(action|prepare|render)(.+)/i',
                    $method->name,
                    $matches
                )) {
                    $name = strtolower($matches[2]);
                    if( !isset($views[$name]) )
                        $views[$name] = array();
                    $views[$name][] = $method;
                }
            }

            $nodes = array();

            foreach ($views as $action => $vrefs) {
                $skip = FALSE;
                foreach ($vrefs as $vref) {
                    $callback = Annotations::get($vref, 'callback');
                    if ($callback) {
                        $result = call_user_func($callback, $this);
                        if (is_array($result)) {
                            $nodes = array_merge($nodes, $result);
                        } elseif ($result instanceof SiteNode) {
                            array_push($nodes, $result);
                        }
                        $skip = TRUE;
                    }
                }
                /*
                 * If we ran callback, there is no need for new SiteNode
                 */
                if (!$skip) {
                    $node = new SiteNode(
                        str_replace('Presenter', '', $pref->getName()),
                        $action
                    );
                    $node->processReflections($pref, $vrefs);
                    array_push($nodes, $node);
                }
            }

        }
        return $nodes;
    }

    public function processRequest($request) {
        $presenter = $request->getPresenterName();
        $action = $request->params['action'];
        $_params = $request->getParams();
        foreach ($this->getObjects(TRUE) as $object) {
            /* last param is some user-define unique id to prevent object conflicts */
            //list($name, $action) = explode(self::NAME_SEPARATOR, $object->name, 3); old way
            if ($presenter == $object->presenter && $action == $object->params['action']) {

                if (empty($_params)) {
                    return $object;
                }
                $params = $object->getParams();
                if (empty($params) || array_intersect($params, $_params) == $params) {
                    return $object;
                }
            }
        }
        return FALSE;
    }

    public function getFiles() {
        return $this->files;
    }

}

Interakce aplikace se stromem:

<?php
final class SitemapModel extends Object {

    protected $site;

    protected $current;

    public function __construct() {
        $cache = Environment::getCache('Sitemap');
        /*
         * Total time (including getPath):
         * ~300 ms  - without cache
         * ~30  ms  - with cache -> improvement
         */
        if(Environment::isProduction() && isset($cache['data'])) {
            $this->site = $cache['data'];
        }
        else
        {
            $this->site = new Site;
            $cache->save('data', $this->site, array(
                Cache::FILES => array(
                    // Special file, when touched, invalidates cache
                    Environment::getConfig('triggers')->sitemap
                ) + $this->site->files
            ));
        }
    }

    public function getPath() {
        $path = array();
        $obj = $this->getCurrent();
		do {
			array_unshift($path, $obj);
			$obj = $obj->getParent();
		} while ($obj instanceof SiteNode);

        return $path;
    }

    private static function sortIndexable($l, $r)
    {
        return ($l['name'] > $r['name']);
    }

    public function getIndexable(SiteNode $node = NULL) {
        if ($node instanceof SiteNode) {
            if ($node->getIndex()) {
                $children = array();
                foreach($node->getObjects(FALSE) as $object) {
                    if ($object->index !== FALSE) {
                        $children[] = $this->getIndexable($object);
                    }
                }
                $indexable = array(
                    'name' => $node->getHeader(),
                    'title' => $node->getTitle(),
                    'presenter' => str_replace('_', ':',$node->getPresenter()),
                    'params' => $node->getParams()
                );
                if (!empty($children)) {
                    usort($children, array(__CLASS__, 'sortIndexable'));
                    $indexable['children'] = $children;
                }
                return $indexable;
            } else {
                return NULL;
            }
        } else {
            $output = array();
            foreach($this->site->getObjects(FALSE) as $object) {
                $indexable = $this->getIndexable($object);
                if ($indexable) array_push($output, $indexable);
            }
            usort($output, array(__CLASS__, 'sortIndexable'));
            return $output;
        }
    }

    public function getCurrent() {
        if (!$this->current) {
            $this->current = $this->site->processRequest(
                Environment::getApplication()
                    ->getPresenter()
                    ->getRequest()
            );
        }
        return $this->current;
    }

}

Pomůcky (componentcontainer nešel serializovat):

<?php
class RecursiveSiteNodeIterator extends RecursiveArrayIterator
{

	/**
	 * Has the current element has children?
	 * @return bool
	 */
	public function hasChildren()
	{
		return $this->current() instanceof Container;
	}



	/**
	 * The sub-iterator for the current element.
	 * @return RecursiveIterator
	 */
	public function getChildren()
	{
		return $this->current()->getObjects();
	}

}

/**
 * Very simple container
 * @internal Until components start supporting serialization/unserialization
 */

class Container extends Object {

    private $name = NULL;

    protected $objects = array();

    protected $parent = NULL;

    const NAME_SEPARATOR = ':';

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function addObject($object)
    {
        $this->objects[$object->name] = $object;
        $object->setParent($this);
    }

    public function removeObject($name)
    {
        $object = $this->getObject($name);
        $object->setParent(NULL);
        unset($this->objects[$object->name]);

    }

    public function getObject($name) {
        return $this->objects[$name];
    }

    public function findObject($name) {
        foreach ($this->getObjects(TRUE) as $object) {
            if ($object->name == $name) return $object;
        }
        return FALSE;
    }

    final public function getObjects($deep = FALSE) {
		$iterator = new RecursiveSiteNodeIterator($this->objects);
        if ($deep)
        {
            $deep = $deep > 0 ? RecursiveIteratorIterator::SELF_FIRST : RecursiveIteratorIterator::CHILD_FIRST;
            $iterator = new RecursiveIteratorIterator($iterator, $deep);
        }
		return $iterator;
    }

    public function setParent($parent)
    {
        if ($parent instanceof Container) {
            $this->parent = $parent;
        } else {
            //throw
        }
        return $this;
    }

    public function getParent()
    {
        return $this->parent;
    }

    public function getName() {
        return $this->name;
    }
}

Příklad použití:

abstract class BasePresenter extends Presenter {
//..
    protected function createTemplate()
    {
        $t = parent::createTemplate();
//..
        $this->sitemap = new SitemapModel; // to by se melo predelat na sluzbu
        $current = $this->sitemap->getCurrent();
//..
        $t->title = $current->title;

        return $t;
    }
}
//..
class SitemapPresenter extends BasePresenter {
    private function getTree($array)
    {
        $el = Html::el('ul');
        foreach($array as $item) {
            $item = (object) $item;
            $li = $el->create('li');
            if ($item->presenter) {
                $li->create('a')->href(
                    $this->link(':' . $item->presenter . ':' . $item->params['action'], $item->params)
                )->title($item->title)->setText(empty($item->name) ? $item->title : $item->name);
            } else {
                $li->setText(empty($item->name) ? $item->title : $item->name);
            }
            if(isset($item->children)) {
                $li->add($this->getTree($item->children));
            }
        }
        return $el;
    }

    /**
     * @index
     * @title('Sitemap')
     * @header('Sitemap')
     */
    public function renderDefault()
    {
        $this->template->header = 'Sitemap';
        $root = Html::el('ul');
        $li = $root->create('li');
        $li->create('a')
            ->href($this->link(':Front:default'))
            ->title('Mauritius Restaurants.mu')
            ->setText('Home');
        $li->add(
            $this->getTree(
                $this->sitemap->getIndexable()
            )
        );
        $this->template->map = $root;
    }
}
redhead
Člen | 1313
+
0
-

Nečetl jsem celý kód, ale moc se mi nelibí to tvoření html objektů v presenteru.. Proč to nenechat na šabloně??