Łatwy link do strony użytkownika [KO3.2]

W serwisach społecznościowych, ale i nie tylko ważny jest adres strony do profilu użytkownika. Standardowo w kohanie routing wygląda tak: example.com/kontroler/akcja/parametr
Chyba łatwiej zamiast:
example.com/user/index/mariusz

lub podobnych, zapamiętać:
example.com/mariusz

Stosując pewne mechanizmy możemy wczytywać kontroler user w miejsce nicku, a następnie uzyskiwać dostęp do akcji:

example.com/mariusz/akcja

Niesie to jednak pewne wymagania. Nazwy użytkowników, którzy będą się rejestrować, nie mogą być takie jak nazwy kontrolerów, które wykorzystujemy do innych celów (np. article – do dodawania artykułów, ponieważ nie moglibyśmy uruchomić kontrolera, lub wyświetlić strony użytkownika o takiej nazwie). Należy przy rejestracji sprawdzać istnienie kontrolerów i jeśli wystąpią to wyświetlać komunikaty w walidacji.

Do dzieła. Zajmijmy się najpierw mechanizmem który wyświetli po wpisaniu użytkownika jako kontroler. W pliku index.php zamieniamy na końcu:

echo Request::factory()
	->execute()
	->send_headers()
	->body();

na

$request = Request::factory();

$controler = $request->controller();
if($controler){
    $class = 'Controller_'.ucfirst($controler);
    if(!class_exists($class)){
        $user = DB::query(1, 'SELECT `id` FROM `users` WHERE `username`="'.$controler.'"')->as_object('Model_User')->as_object()->execute()->current();
        if($user){
            $request = $request->factory('/user/'.($request->action()?$request->action():'index').'/'.$user->id.'/'.$request->param('id').'/'.$request->param('id2'));
        }
    }
}
echo $request
	->execute()
	->send_headers()
	->body();

Najpierw pobieramy nazwę kontrolera z linku

$controler = $request->controller();

Następnie sprawdzamy czy nie istnieje taki kontroler, jeśli nie istnieje wczytamy usera, jeśli istnieje to wczytujemy kontroler

$class = 'Controller_'.ucfirst($controler);
    if(!class_exists($class)){

Wydajemy zapytanie zwracające usera, gdzie username jest username z linku. Jeśli user wystąpi tworzymy request, który wczyta kontroler user, akcję index tgo kontrolera (inną wczyta po pojawieniu się /inna_akcja) i przypisze do parametru id, numer id użytkownika z bazy, i umożliwi przekazanie drugiego parametru

$user = DB::query(1, 'SELECT `id` FROM `users` WHERE `username`="'.$controler.'"')->as_object('Model_User')->as_object()->execute()->current();
        if($user){
            $request = $request->factory('/user/'.($request->action()?$request->action():'index').'/'.$user->id.'/'.$request->param('id').'/'.$request->param('id2'));
        }

Dzięki temu możemy wywołać:

example.com/mariusz/article/2

Będzie to podobne do

example.com/user/article/mariusz/2

I może oznaczać, np. wywołanie widoku, który wyświetli artykuły użytkownika mariusz, które mają typ 2

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_User extends Controller_Default {

    public function action_index() {
        $user_id=$this->request->param('id');
        $user=DB::query(1, 'SELECT * FROM `users` WHERE `id`="'.$user_id.'"')->as_object()->execute();
        //strona użytkownika
    }
    public function action_article() {
        $user_id=$this->request->param('id');
        $type=(int)$this->request->param('id2');
        $articles=DB::query(1, 'SELECT * FROM `articles` WHERE `user_id`="'.$user_id.'" AND `type`="'.$type.'"')->as_object()->execute();
        //lista artykułów
    }
}

Należy jeszcze zabezpieczyć aby użytkownicy, którzy będą się rejestrować nie wybrali zarezerwowanych nazw.

Stwórzmy klasę User w /application/classes/user.php

<?php defined('SYSPATH') or die('No direct access allowed.');

class User {

    public static function reserved($nick)
    {
        $unique=TRUE;
        
        $class='Controller_'.ucfirst($nick);
        if(class_exists($class)){
            $unique = FALSE;
        }
        
        $words=array(
            'about',
            'contact',
            'help',
            'logout',
            'terms',);
        foreach ($words as $word){
            if ($nick==$word)
                $unique = FALSE;
        }
        
        //$user=ORM::factory('user')->where('username', '=', $nick)->find_all();
        $user=DB::query(1, 'SELECT * FROM `users` WHERE `username`="'.$username.'"')->as_object()->execute();
        if(count($user)){
            $unique = FALSE;
        }

        return $unique===TRUE ? TRUE : FALSE;
    }
}

Klasa ta zawiera funkcję reserved, która zwraca FALSE, gdy nazwa jest zarezerwowana (przez nazwę kontrolera, przez nazwy podane, lub innego użytkownika), lub TRUE, gdy jest wolna.

Należy ją dodać do validacji przy rejestracji:

$this->template->content=View::factory('register')
    ->bind('errors',$errors)
    ->bind('data',$data);
if(isset($_POST['submit'])){
    $post = Validation::factory($_POST)
         ->rule('username', 'min_length', array(':value', 3))
         ->rule('username', 'User::reserved')
         ->rule('password_confirm', 'matches', array(':validation', 'password', 'password_confirm'))
         ->labels(array('username'=>'Username','password'=>'Password','password_confirm'=>'Password confirm'));

    if($post->check()){
        //save
        unset($_POST);
    }else{
        $errors=$post->errors('messages');
        $data=$_POST;
        unset($_POST);
    }
}

Nie ma tu nic nowego odnośnie walidacji, poza regułą User::reserved. Spowoduje ona przesłanie wprowadzonej nazwy i sprawdzenie czy jest wolna.
Aby wyświetlić ładny komunikat błędu należy stworzyć plik /application/messages/messages.php

<?php defined('SYSPATH') OR die('No direct access allowed.');

return array(
    'username' => array(
        'User::reserved'=> 'This :field is reserved',
    ),
);
?>

Możemy też przetłumaczyć komunikat, jeśli korzystamy z plików językowych. Do /application/i18n/pl.php dodajemy

<?php defined('SYSPATH') or die('No direct script access.');

return array(
    'This :field is reserved'=>':field jest zarezerwowana',
);
?>

To wszystko. Możemy się teraz cieszyć łatwymi linkami do stron użytkownika. Przy okazji poznaliśmy też sposób na sprawdzanie unikalności nicku.

25 Odpowiedzi :“Łatwy link do strony użytkownika [KO3.2]”

  1. E… a nie prościej użyć Route tak jak bozia nakazała, tylko wrzucić tam lambda function?

    Tak na szybko:

    Route::set('user', function($uri) {
        $elements = explode('/', $uri);
        $user = DB::select()->from('users')->where('username','=',$elements[0])->execute();
        if($user->count() > 0)
        {
            return array(
                'controller' => 'user',
                'action' => $elements[1],
                'user' => $user->id,
            );
        }
    });
    

    Przy okazji jeszcze:

     $user=DB::query(1, 'SELECT * FROM `users` WHERE `username`="'.$username.'"')->as_object()->execute();
    

    To jest potworek, którego nigdy, przenigdy nie powinno się stosować. W Kohanie mamy zarówno prepared statements jak i query buildera. W tym pierwszym przypadku kod powinien wyglądać następująco:

    $query = DB::query(Database::SELECT, 'SELECT * FROM `users` WHERE username = :user')->param(':user', $username)->as_object()->execute();
    

    W tym drugim:

    $query = DB::select()->from('users')->where('username','=',$username)->as_object()->execute();
    

    Użycie obu tych technik pilnuje, żeby wszystkie zewnętrzne dane przesyłane do SQL były poprawnie eskejpowane.

  2. Mariusz napisał:

    @d4rky fajny przykład :) i do eskejpowania SQL masz świętą rację.
    Co w przypadku, gdy user poda nick taki jak nazwa jakiegoś kontrolera (gdyby nie było sprawdzane przy rejestracji, albo kontroler został dodany później)? Routing ten powinien być na końcu, ale czy wcześniej nie wczyta się defaultowy?

  3. Cóż, to jest pewien problem, aczkolwiek jeśli stosujemy już routing domena.pl/user to powinniśmy się liczyć z możliwością problemów tego sortu ;) Znacznie rozsądniejszym rozwiązaniem jest wtedy domena.pl/u/user chociażby.

    Jeśli jednak bardzo, baaaaardzo nam zależy na tego typu rozwiązaniu, dlaczego nie zrobić tego najprościej?

    if(class_exists('Controller_'.ucfirst($name)))
    {
        return false;
    }

    Z drugiej strony tego sortu rozwiązanie zatnie się jeśli mamy bardziej skomplikowany niż default routing, bo sprawdza jedynie istnienie kontrolera ;) Ale tak jak mówiłem wrzucanie nazwy użytkownika do globalnej przestrzeni nazw po prostu się prosi o kłopoty.

    Przy okazji, dopisz do szablonu jakąś instrukcję jak się tutaj coś formatuje, bo bez Markdown się gubię.

  4. morawcik napisał:

    Kolejny świetny i przydatny artykuł! Dzięki!

    PS Masz w zamiarze napisać coś o własnych modułach (jak to się robi itp) ?

    • Mariusz napisał:

      Coś mógłbym napisać, aczkolwiek ostatnio znów mam mało czasu, stąd również mało wpisów się pojawia :( Jeśli potrzebujesz coś pilnie, to mógłbyś popatrzyć jak jest zbudowany moduł Gmaps (w miarę prosto napisany i można w analogiczny sposób stworzyć własny) i jak go używać.

  5. tajger napisał:

    Witam!
    Można się jakość skontaktować z autorem tej bloga?
    Chciałbym się zapytać czy mógły Pan zrobić tutorial jak zrobić bloga internetowego od początku w kohanie z możliwością dodawania komentarzy

    • Mariusz napisał:

      To ja :) myślałem już o tym, aczkolwiek ostatnio trochę mam mało czasu. W razie gdyby pojawiła się nowa wersja frameworka, albo jak znajdę trochę wolnego czasu to coś spróbuję zmajstrować i opisać.

      • tajger napisał:

        dla Pana to pewnie minut kilkanaście zrobić taki blog z dodawaniem wpisów i komentarzy pod danym wpisem.. właściwie widziałem też ostatnio kilka osób na forach proszących o taki właśnie malutki blog napisany w Kohanie, tutaj tak samo widziałem chyba 1 lub 2 userów co potrzebują coś takiego na swoje potrzeby.. także więc ja też chciałbym poprosić jeśli może Pan napisać mały tutorial Blog Kohana.. moim zdaniem dziwię się nadal, że nigdzie na żadnych forach i innych tutorialach nie ma czegoś takiego, a ludzie, którzy wykorzystują Kohanę to na pewno im się to przyda.. pozdrawiam serdecznie (btw. nigdy nie widziałem takiego blogu jaki Pan ma)

  6. Uff… ale żeś zakręcił. Jest jeszcze jedno rozwiązanie oparte o directory, które dodatkowo nie jest obciążone zadawanie zawsze zapytania do bazy. Alias zczytywany jest jako paramter. Routing ustawiony przed defaultowym – w a więc sprawdzanie następuje tylko i wyłącznie gdy nie ma innego routa. W directory mamy wszystko co odpowiada za usera – jego theme, dane, etc. Wszystko za pomocą tego co daje KO :)

  7. A co zrobić by mieć kontroler w folderze?

    Np. controller/admin/admin.php

    • Mariusz napisał:

      W /application/bootstrap.php dodajemy:

      Route::set('admin', 'admin(/<controller>(/<action>(/<id>(/<id2>))))')
              ->defaults(array(
                      'directory'  => 'admin',
                      'controller' => 'default',
                      'action'     => 'index',
      ));
      

      Następnie dodajemy np. kontroler default do folderu /admin
      /application/classes/controller/admin/default.php

      <?php defined('SYSPATH') or die('No direct script access.');
      
      class Controller_Admin_Default extends Controller_Template {
      

      PS. W skrypcie blogu, który ostatnio dodałem jest to wykorzystane.

  8. Piotr (Delavor) napisał:

    Cześć, zaczynam dopiero poznawać kohanę i zastanawia mnie jedna rzecz. Zrobienie linku /user/id/username, w jaki sposób? Próbowałem podpatrzeć jak to wygląda w twoim blogu, ale nadal nie rozumiem na jakiej zasadzie to działa. Gdy podaję link bez username to działa bez problemu, natomiast z wywala błąd

    HTTP_Exception_404 [ 404 ]: Unable to find a route to match the URI: user/details/1/Delavor

    • Mariusz napisał:

      Witaj, komunikaty powinny Cię nakierowywać, skoro jednak zaczynasz to możesz nie wiedzieć, że standardowy routing to /kontroler/akcja/parametr, aby można było przesyłać drugi parametr (np.nazwa użytkownika) należy w bootstrapie zmienić:

      Route::set('default', '(<controller>(/<action>(/<id>)))')
          ->defaults(array(
                  'controller' => 'default',
                  'action'     => 'index',
          ));
      

      na

      Route::set('default', '(<controller>(/<action>(/<id>(/<id2>))))')
          ->defaults(array(
                  'controller' => 'default',
                  'action'     => 'index',
          ));
      
  9. dev napisał:

    Witam,
    to odkopie posta i się podłączę… piszę sklep internetowy na ko3.2 i mam kłopot: jak rozpisac routing aby:

    1. klikając na mojadomena.com/nazwakategorii/nazwaproduktu pokazywał się kontroler: produkty/pokazprodukt/nazwaproduktu
    2. klikając na mojadomena.com/nazwakategorii pokazywal sie kontroler produkty/pokazkategorie

    siedze nad tym ładnych pare dni i nie jestem w stanie sobie z tym poradzić.

    pozdrawiam

    • Mariusz napisał:

      Mógłbyś zrobić podobnie jak tu. W pliku index.php pobierać nazwę kontrolera, akcji i sprawdzać czy takowe występują, jeśli tak to wczytywać je, jeśli nie to sprawdzać czy jest taka kategoria (kontroler) i produkt (akcja). Kontrolery i akcje miałyby pierwszeństwo przed kategoriami i produktami, a sprawdzanie tych drugich nie obciążałoby zbytnio serwera zbędnymi zapytaniami.

      Problem może być, gdyż w nazwach mogą występować spacje i polskie znaki, trzeba by skorzystać zapewne z URL::title() (np. w bazie podczas dodawania zapisywać pole name i name_seo);

  10. Także, bawiłem się troszeczkę tym wszystkim, jednak w innym zastosowaniu,
    Dla mnie jest nie wygodne, żeby zmienić w razie potrzeby domyślną akcje w pliku index.php tak samo jako dodanie nowym parametrów, ja przeniosłem tą obsługę do bootstrap, tam gdzie tego miejsce.

    zmienna request wyglada tak:

     $request = $request->factory('/user'.$request->url());
    

    i dodałem routing

    Route::set('user', 'user(/<name>(/<controller>(/<action>(/<id>))))')
        ->defaults(array(
                    'directory'  => 'user',
                    'controller' => 'default',
                    'action'     => 'index',
            )); 
    

    Z tym, że u mnie profil użytkownika jest rozbudowany i jest tak jakby subportalem na mojej stronie, także wszystkie controllery obsługujące profil są w oddzielnym folderze.

  11. Pytanie trochę z innej beczki, próbuje zrobić coś na zasadzie, że każdy profil, może mieć własny widok.

    próbowałem tak

    public $template = Model::factory("user")->get_templates();
    

    jednak wyrzuca błędy parsowanie, może doradzisz jak można by to rozwiązać bo jeszcze nie ukrywając jestem troche noobem z kohana no i z programowania obiektowego też nie świece przykładem ;) .

    • Mariusz napisał:

      Jeśli szablonów masz kilka to mógłbyś we views utworzyć takie katalogi i w bazie przechowywać co user wybrał.
      Potem

          $template = $user->template;
          $this->template = View::factory($template);
      
      • Trochę źle napisałem.
        Jak pisałem, wcześniej każdy profil to jest jakby subportal, wszystkie klasy do profilu trzymam w oddzielnym katalogu, również posiadają, własny kontroller defalutowy ponieważ, chcę wprowadzić możliwość jakby zmiany całego layoutu dla każdego użytkownika.

        public tempates to jest zmienna classy controllera default, i właśnie tu, za pomocą metody w modelu get_templates(); wyciągam z tabeli nazwę layoutu i próbuję ją przypisać do
        do

        public $templates =Model::factory('user')-> get_templates;
        

        ErrorException [ Parse Error ]: syntax error, unexpected ‚(‚, expecting ‚,’ or ‚;’

      • Mariusz napisał:

        Zadeklaruj szablon bez wartości:

        public $template;
        

        A w konstruktorze (albo before) w defaultowym lub user kontrolerze przypisz wartość

        $this->template = View::factory(Model::factory('user')-> get_templates());
        

        Nie wiem co zwraca get_templates(); ale powinna nazwę szablonu z views, który user wybrał.

Dodaj komentarz

Dodając kod PHP używaj tagów: [php][/php]

*