Mapa stránek
- amsys
- Člen | 20
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
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
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?
- amsys
- Člen | 20
_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
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;
}
}