Skip to content

Instantly share code, notes, and snippets.

@zofe
Last active August 29, 2015 14:07
Show Gist options
  • Save zofe/e9ef3fdc7ebf9824c8dc to your computer and use it in GitHub Desktop.
Save zofe/e9ef3fdc7ebf9824c8dc to your computer and use it in GitHub Desktop.
Internal router for rapyd-laravel that match "uri" and/or "query string", it can run before Laravel to Fire widget events
<?php namespace Zofe\Rapyd;
/**
* Class Router
* the rapyd router, works "before" laravel router to check uri/query string
* it set widgets status / actions.
*
* @package Zofe\Rapyd
*
* @method public static get($uri=null, $query=null, Array $route)
* @method public static post($uri=null, $query=null, Array $route)
* @method public static patch($uri=null, $query=null, Array $route)
* @method public static put($uri=null, $query=null, Array $route)
* @method public static delete($uri=null, $query=null, Array $route)
*/
class Router
{
public static $routes = array();
public static $qs = array();
public static $remove = array();
public static $methods = array();
public static $callbacks = array();
/**
* little bit magic here, to catch all http methods to define a named route
* <code>
* Router::get(null, 'page=(\d+)', array('as'=>'page', function ($page) {
* //with this query string: ?page=n this closure will be triggered
* }));
* </code>
* @param $method
* @param $params
* @return static
*/
public static function __callstatic($method, $params)
{
self::checkParams($method, $params);
$uri = ltrim($params[0],"/");
$qs = $params[1];
$name = $params[2]['as'];
self::$routes[$name] = $uri;
self::$qs[$name] = $qs;
self::$remove[$name] = array();
self::$methods[$name] = strtoupper($method);
self::$callbacks[$name] = $params[2][0];
return new static();
}
/**
* dispatch the router: check for all defined routes and call closures
*/
public static function dispatch()
{
$uri = \Request::path();
$qs = parse_url(\Request::fullUrl(), PHP_URL_QUERY);
$method = \Request::method();
foreach (self::$routes as $name=>$route) {
$matched = array();
if ($route=='' || preg_match('#' . $route . '#', $uri, $matched) && self::$methods[$name] == $method) {
array_shift($matched);
if (self::$qs[$name]!='' && preg_match('#' . self::$qs[$name] . '#', $qs, $qsmatched)) {
array_shift($qsmatched);
$matched = array_merge($matched, $qsmatched);
call_user_func_array(self::$callbacks[$name], $matched);
} elseif (self::$qs[$name] == '') {
call_user_func_array(self::$callbacks[$name], $matched);
}
}
}
}
public static function isRoute($name, $params)
{
$uri = \Request::path();
$qs = parse_url(\Request::fullUrl(), PHP_URL_QUERY);
$method = \Request::method();
$route = @self::$routes[$name];
$matched = array();
if ($route=='' || preg_match('#' . $route . '#', $uri, $matched) && self::$methods[$name] == $method) {
array_shift($matched);
if (self::$qs[$name]!='' && preg_match('#' . self::$qs[$name] . '#', $qs, $qsmatched)) {
array_shift($qsmatched);
$matched = array_merge($matched, $qsmatched);
if ($matched == $params)
return true;
} elseif (self::$qs[$name] == '') {
if ($matched == $params)
return true;
}
}
return false;
}
/**
* check if route method call is correct
* @param $method
* @param $params
*/
private static function checkParams($method, $params)
{
if (! in_array($method, array('get','post','patch','put','delete')))
throw new \BadMethodCallException("valid methods are 'get','post','patch','put','delete'");
if (count($params)<3 ||
!is_array($params[2]) ||
!array_key_exists('as', $params[2]) ||
!array_key_exists('0', $params[2]) ||
!($params[2][0] instanceof \Closure) )
throw new \InvalidArgumentException('third parameter should be an array containing a
valid callback: array(\'as\'=>\'routename\', function () { }) ');
}
/**
* queque to remove from url one or more named route
*
* @return static
*/
public function remove()
{
$routes = (is_array(func_get_arg(0))) ? func_get_arg(0) : func_get_args();
end(self::$routes);
self::$remove[key(self::$routes)] = $routes;
return new static();
}
/**
* return a link starting from routename and params
* like laravel link_to_route() but working with rapyd widgets/params
*
* @param $name route name
* @param $param one or more params required by the route
* @return string
*/
public static function linkRoute($name, $params, $url = null)
{
$url = ($url != '') ? $url : \Request::fullUrl();
$url_arr = explode('?', $url);
$url = $url_arr[0];
$qs = (isset($url_arr[1])) ? $url_arr[1] : '';
if (!is_array($params)) $params = (array)$params;
//If required we remove other routes (from segments or qs)
if (count(self::$remove[$name])) {
foreach (self::$remove[$name] as $route) {
if (self::$routes[$route])
$url = preg_replace('#(\/?)'.self::$routes[$route].'#', '', $url);
if (self::$qs[$route]) {
$url = preg_replace('#(&?)'.self::$qs[$route].'#', '', $url);
}
}
}
//if this route works with uri
//I check for all params to build the append segment with given params,
//then I strip current rule and append new one.
if (self::$routes[$name]) {
$append = self::$routes[$name];
if (preg_match_all('#\(.*\)#U',$append, $matches)) {
foreach ($params as $key=>$param) {
$append = str_replace($matches[0][$key],$param, $append);
}
}
$url = preg_replace('#(\/?)'.self::$routes[$name].'#', '', $url);
$url = ltrim($url."/".$append,'/');
}
//if this route works on query string
//I check for all params to buid the "append" string with given params,
//then I strip current rule and append the new one.
if (self::$qs[$name]) {
$append = self::$qs[$name];
if (preg_match_all('#\(.*\)#U',$append, $matches)) {
foreach ($params as $key=>$param) {
$append = str_replace($matches[0][$key],$param, $append);
}
}
$qs = preg_replace('#(&?)'.self::$qs[$name].'#', '', $qs);
$qs = str_replace('&&','&',$qs);
$qs = rtrim($qs, '&');
$qs = $qs .'&'.$append;
}
if ($qs != '') $qs = '?'.$qs;
return $url.$qs;
}
}
.......
//example
\Zofe\Rapyd\Router::get(null, null, array('as'=>'current', function() {
//will be execuded every request (no uri and no query if filtered)
}));
## DataGrid
\Zofe\Rapyd\Router::get(null, 'ord=(-?)(\w+)', array('as'=>'orderby', function($direction, $field) {
//orderby will be executed for every request where there is a "ord" parameter
\Event::queue('dataset.sort', $direction, $field);
}))->reset('page');
\Zofe\Rapyd\Router::get(null, 'page=(\d+)', array('as'=>'page', function($page) {
\Event::queue('dataset.page', $page);
}));
## DataForm
\Zofe\Rapyd\Router::post(null, 'process' , array('as'=>'process', function() {
\Event::queue('dataform.save');
});
## DataEdit
\Zofe\Rapyd\Router::post(null, 'insert=(\d+)', array('as'=>'insert', function($id) {
\Event::queue('dataedit.insert', $id);
});
\Zofe\Rapyd\Router::patch(null, 'update=(\d+)', array('as'=>'update', function($id) {
\Event::queue('dataedit.update', $id);
});
\Zofe\Rapyd\Router::get(null, 'delete=(\d+)', array('as'=>'delete', function($id) {
//delete confirmation
\Event::queue('dataedit.delete', $id);
})->persist(false);
\Zofe\Rapyd\Router::delete(null, 'do_delete=(\d+)', array('as'=>'do_delete', function($id) {
\Event::queue('dataedit.dodelete', $id);
});
//Alternative o variazioni possibili:
## DataGrid
\Zofe\Rapyd\Router::get('/ord/(-?)(\w+)', null , array('as'=>'orderby', function($direction, $field) {
//orderby will be executed for all request where there is a uri segment /ord/field or /ord/-field
});
\Zofe\Rapyd\Router::get('/page/(\d+)', null, array('as'=>'page', function($page) {
//limit offset
});
##### altri parametri..
/*
indica di non preservare i parametri delle route indicate
(es. la route dell'orderby resetta la paginazione)
nota: per default tutte le route post/patch vengono rimosse sempre.
*/
->remove('nomeroute', 'nomealtraroute'...):
@zofe
Copy link
Author

zofe commented Oct 2, 2014

il "router" (anche se è improprio chiamarlo così) serve a centralizzare la logica degli eventi e degli stati dei widgets, ed a creare le url internamente con un meccanismo simile alle named-routes (ti dico la route, ti passo i parametri e mi costruisci il link).
Serve anche a personalizzare completamente le uri.. es: non ti piace la query string? usa le uri (anche scavalcando il router di laravel)
funziona così:

  • gira prima di quello di laravel, il dispatch viene fatto nel service provider, non fà redirect e non è bloccante: anche piu' di una route puo' essere verificata es. http://www.rapyd.com/rapyd-demo/grid?ord=-author_id&page=2 ne matcha/esegue 2.
  • fa un match fra http method, uri e query string, nel caso si verifichi il match esegue la closure..
  • la closure viene usata per fare queue di eventi relativi ai widget, usando la Event di laravel (Mi piacerebbe creare una piccola classe event mia per fare esperienza, ma va bene così.)

@zofe
Copy link
Author

zofe commented Oct 2, 2014

obiettivo attuale..

  • fare named route, obbligatoriamente (per poterle lavorare nei widget devono avere un nome)
  • il terzo parametro potenzialmente deve poter essere solo la closure da eseguire (per compatibilità con il router di laravel)
<?php
\Zofe\Rapyd\Router::get(null, 'ciccio=(\d+)',  function($x) {
    die('hai richiesto ?ciccio='.$x);
}));
  • deve essere possibile definire una route con un array, per semplificare la possibilità di salvarli in un file di configurazione, sovrascrivibile dagli utenti finali es:
<?php
Zofe\Rapyd\Router::route(array(
    'method'=>'patch',
    'qs'=>'update=(\d+)',
    'as'=>'update', 
    'remove'=>array('orderby'), 
   'action'=>function($id) {
    //closure..
}));
  • prevedere oltre al ->persist(false); un ->remove(array(named.route1, named.route2)); per permettere la rimozione di uno o più parametri (es. se sto' costruendo il link per fare l'orderby, devo necessariamente resettare la paginazione, e via dicendo).. verificare la possibilità di fare un removeAll()
  • i link interni andranno fatti così (già funziona)
<?php
$orderby = Rapyd::linkRoute('orderby', '-', 'title');
// on /rapyd-demo/grid?ord=-id&page=2&custom=3
// will output  /rapyd-demo/grid?custom=3&ord=-title
  • spostare la logica degli "sniff action/status" nelle closure :
    al momento nelle closure ho previsto queue di eventi dei vari widget.
    Poi nei widget andranno creati i singoli metodi e i listener, poi al build o nel costruttore.. a seconda dei casi ci và un Event::flush('nomewidget.*') per scatenare gli eventi.

In questo modo si dovrebbe riuscire a fare a meno del controller, e (poichè si usano gli eventi) dovrebbe essere semplice, per chi ne ha la necessità, controllare il comportamento dei widget, e "crearne di nuovi".

@zofe
Copy link
Author

zofe commented Oct 3, 2014

problema con paginazione & dataset (come scavalcare il paginator di laravel)
risolto con nel listener:

Paginator::setCurrentPage($pag)

e l'override di link nel dataset

$links = $this->paginator->links($view);
$links = preg_replace('@href="(.*\?page=(\d+))"@U', 
                  'href="'.Rapyd::linkRoute('page', '$2').'"', $links);

@zofe
Copy link
Author

zofe commented Oct 4, 2014

sembra funzionare bene tutta la logica.. si dovrebbe fare un handler ma al momento basta:

  • creare i metodi nei widget
  • fare i listen sul metodo statico principale (source)
  • e al build fare il flush/fire degli eventi:
<?php
...

    public function sort($direction, $field)
    {
       ...
    }

    public function page($page)
    {
        \Paginator::setCurrentPage($page);
    }

    public static function source($source)
    {
        $ins = new static();
        $ins->source = $source;

        \Event::listen('dataset.sort', array($ins, 'sort'));
        \Event::listen('dataset.page', array($ins, 'page'));
       ....
     }

    public function build()
    {
        \Event::flush('dataset.sort');
        \Event::flush('dataset.page');
        ....

@zofe
Copy link
Author

zofe commented Oct 6, 2014

Limiti:
Ora come ora.. si è limitati comunque all'uso della PK nelle route di CRUD, perchè il "find" dei record viene appunto fatto su id.

Laravel di suo ha una roba potentissima che è il Route model binding, http://laravel.com/docs/4.2/routing#route-model-binding
per fare DI del model, definendo eventualmente il modo con cui viene caricato model.
Si puo' trovare un modo di sfruttarlo?

@zofe
Copy link
Author

zofe commented Oct 12, 2014

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment