PHP – operacje na bitach w praktyce

Podziel się z innymi!

    Na początku mojej nauki PHP kupiłem sobie książkę „PHP4 Aplikacje” (Tobiasa Ratschiller i Till Gerken – Wydawnictwo Robomatic). Zawarta w tej lekturze tematyka była wtedy dla mnie zbyt zaawansowana i potem wielokrotnie wracałem do tej pozycji stopniowo dojrzewając do poruszanych w niej tematów. Najdłużej wzbraniałem się przed zgłębieniem wiedzy dotyczącej operacji na bitach. Dzisiaj nie wiem właściwie dlaczego bo zagadnienie jest całkiem proste, a rozwiązania oparte o system binarny mają wiele zalet.

    Najpowszechniej chyba spotykanym przypadkiem stosowania wartości bitowych są wszelkiego rodzaju systemy uprawnień. Każdy chyba programista PHP zaprzyjaźnił się z dyrektywami

    ini_set('display_errors', 1);
    ini_set('error_reporting', E_ALL);

    Druga z wymienionych dyrektyw ma też odpowiadającą jej funkcję „error_reporting”, która jako argument przyjmuje pozom raportowania błędów. Poziom ten można przekazać w postaci maski bitowej złożonej ze stałych odzwierciedlających wartości przypisane poszczególnym rodzajom błędów PHP.

    Wartość bitowe stałych nie są jak widać w formacie binarnym tylko dziesiętnym.

    stała zapis w formacie dwójkowym (binarnym) zapis w formacie dziesiętnym
    E_ERROR 00000001 1
    E_WARNING 00000010 2
    E_PARSE 00000100 4
    E_NOTICE 00001000 8

    Do przeliczania wartości binarnych na dziesiętne służy w PHP funkcja bindec, a z dziesiętnych na binarne decbin.

    I tak wywołanie …

    error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);

    spowoduje że wyświetlane będą wszystkie błędy czasu wykonania, ostrzeżenia, błędy parsowania oraz uwagi.

    Z kolei użycie takiej maski …

    error_reporting(E_ALL &~ E_NOTICE);

    pozwoli na ukrycie wszystkich mało ważnych uwag. Natomiast wszystkie pozostałe błędy i bardziej ważne ostrzeżenia będą dalej raportowane.

    W celu zrozumienia wyżej zaprezentowanych operacji niezbędnym będzie poznanie operatorów logicznych. Ambitnych z kolei odsyłam do Algebry Boole’a, choć nie zachęcam zbyt mocno.

    Czasami zbyt gruntowny wykład teoretyczny utrudnia zrozumienie prostych spraw, które podawane w małych porcjach powoli poszerzają horyzonty. Czytałem kilka prac poświęconych operacjom na bitach, w których była cała masa operacji na 0 (zerach) i 1 (jedynkach), wiele tabel, wzorów, równań. Wiele z nich wspominało o prawach De Morgana czy postulatach Huntigtona, ale mało który materiał traktował o tym jak tego używać. Dopiero niedawno kolega podesłał mi link do artykułu, który podchodzi do tematu z praktycznej strony.

    Nospor w jednym z wpisów na swoim blogu pt. opcje dwuwartościowe prezentuje studium przypadku użycia operacji bitowych. Proponuje zastąpienie kilku flag – czyli dwustanowych pól przyjmujących wartość logiczną TRUE lub FALSE – w tabeli w bazie danych, jednym polem przechowującym wartość bitową.

    Polega to konkretnie na tym, że zamiast mieć w tabeli trzy pola i trzy indeksy:

    CREATE TABLE `offer` (
      `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
      `name` VARCHAR(32) NOT NULL,
      `is_active` tinyint UNSIGNED NOT NULL DEFAULT 0,
      `is_promotion` tinyint UNSIGNED NOT NULL DEFAULT 0,
      `is_sale` tinyint UNSIGNED NOT NULL DEFAULT 0,
      PRIMARY KEY  (`id`),
      KEY `is_active` (`is_active`),
      KEY `is_promotion` (`is_promotion`),
      KEY `is_sale` (`is_sale`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

    Można je zastąpić jednym polem i jednym indeksem.

    CREATE TABLE `offer` (
      `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
      `username` VARCHAR(32) NOT NULL,
      `options` tinyint UNSIGNED NOT NULL DEFAULT 0,
      PRIMARY KEY  (`id`),
      KEY `options` (`options`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

    Dla trzech, niezależnych od siebie opcji powyższe rozwiązanie nie jest zbyt wygodne, ale dla potrzeb edukacyjnych wystarczy. Niżej pokazuję bardziej adekwatny przypadek użycia, tymczasem wyjaśniam że aby móc zapisać w jednym polu wartości kilku opcji trzeba najpierw każdej opcji przypasować bit.

    // Normalnie staram się nie nadużywać stałych 
    // jednak tutaj zrobiłem to dla jasności przykładów
    define('OPT_NONE', 0);_
    define('OPT_IS_ACTIVE', 1);
    define('OPT_IS_PROMOTION', 2);
    define('OPT_IS_SALE', 4);

    Przy okazji chciałbym wspomnieć że poniższe zapisy są tożsame.

    1 1 << 0 bindec(‚00000001’)
    2 1 << 1 bindec(‚00000010’)
    4 1 << 2 bindec(‚00000100’)

    Wiedząc jaki bit oznacza jaką opcję możemy zapisać tę informację w bazie danych. W tym celu trzeba zsumować poszczególne opcje przy pomocy logicznego operatora OR „|”.

    // Wszystkie opcje
    $options = (OPT_IS_ACTIVE | OPT_IS_PROMOTION | OPT_IS_SALE);
    // otrzymana wartość to 7
     
    // Oferta jest aktywna i w wyprzedaży, ale nie w promocji
    $options = (OPT_IS_ACTIVE | OPT_IS_SALE);
    // otrzymana wartość to 5
     
    // Oferta jest w promocji, ale nie jest aktywna ani w wyprzedaży
    $options = OPT_IS_PROMOTION;
    // otrzymana wartość to 2

    Kiedy już przypiszemy ofercie jakieś opcje chcielibyśmy sprawdzić czy takową posiada.

    // Oferta jest aktywna i w wyprzedaży, ale nie w promocji
    $options = (OPT_IS_ACTIVE | OPT_IS_SALE);
     
    // Upewniamy się czy oferta jest aktywna
    ($options & OPT_IS_ACTIVE) > 0 ? 'yes' : 'no';
    // otrzymany wynik yes
     
    // Sprawdzamy czy jest w promocji
    ($options & OPT_IS_PROMOTION) > 0 ? 'yes' : 'no';
    // otrzymany wynik no
     
    // Sprawdzamy czy jest aktywna i w wyprzedaży
    ($options & (OPT_IS_ACTIVE | OPT_IS_SALE)) == ((OPT_IS_ACTIVE | OPT_IS_SALE)) ? 'yes' : 'no';
    // otrzymany wynik yes
     
    // Sprawdzamy czy jest w promocji i/lub w wyprzedaży
    ($options & (OPT_IS_PROMOTION | OPT_IS_SALE)) > 0 ? 'yes' : 'no';
    // otrzymany wynik yes
     
    // Sprawdzamy czy jest w promocji albo w wyprzedaży
    ($options & (OPT_IS_PROMOTION | OPT_IS_SALE)) != (OPT_IS_PROMOTION | OPT_IS_SALE) ? 'yes' : 'no';
    // otrzymany wynik yes

    Proszę zwrócić uwagę na zastosowane nawiasy. Operatory logiczne mają priorytet niższy od operatorów arytmetycznych, a także od operatorów porównania więc jeśli wykonamy test (2 | 4 == 6) to z pewnością otrzymamy inny wynik niż jeśli zastosujemy następujący zapis ((2 | 4) == 6)

    Opcje możemy modyfikować

    // Oferta jest aktywna i w wyprzedaży, ale nie w promocji
    $options = (OPT_IS_ACTIVE | OPT_IS_SALE);
    // 5
     
    // Dodajemy opcję w promocji
    $options |= OPT_IS_PROMOTION;
    // 7
     
    // Usuwamy opcję w wyprzedaży
    $options &= ~OPT_IS_SALE;
    // 3
     
    // Usuwamy też opcję jest aktywna
    $options ^= OPT_IS_ACTIVE;
    // 2
     
    // I dodajemy ją spowrotem
    $options ^= OPT_IS_ACTIVE;
    // 3

    Jak widać w ostatnim przykładzie operator ^ jest przełącznikiem, który usuwa bit jeśli jest ustawiony i ustawia jeśli nie jest ustawiony.

    Przykład opcji – zapożyczony zresztą od Nospora – nie prezentuje pełnego potencjału bitów, które nie bez powodu są często używane przy konstruowaniu wszelkiego rodzaju systemów uprawnień. Oprócz oszczędności miejsca za pomocą szablonów bitowych można w łatwy sposób zaimplementować dziedziczenie uprawnień.

    Stwórzmy kilka stref dostępu, a następnie grupy użytkowników, o różnych poziomach uprawnień pozwalających na dostęp do poszczególnych stref.

    strefy dostępu

    • Strefa dla wszystkich – dostęp do niej powinni mieć wszyscy użytkownicy.
    • Strefa dla użytkowników uwierzytelnionych. Zalogowani użytkownicy powinni mieć dostęp do tego co użytkownicy anonimowi oraz do kilku innych funkcjonalności
    • Strefa dla moderatorów – moderatorzy mogą z założenia wszystko to co użytkownicy zalogowani, ale mają też funkcje edycyjne.
    • Strefa dla sponsorów – ich pole obejmuje zakres aktywności użytkowników zalogowanych oraz częściowo pokrywa się ze strefą moderatora. Nie mogą jednak edytować treści, za to mają dostęp do raportów, które zwykły moderator nie ma prawa widzieć
    • Administrator jak to zwykle bywa może wszystko

    Przypiszmy każdej ze stref bit.

    $for_logged = 1;
    $for_moderators = 2;
    $for_sponsors = 4;
    $for_administrators = 8;

    Następnie poszczególnym grupom użytkowników ustawmy taki szablon bitowy, który pozwoli im na dostęp do określonych stref. Przy pomocy szablonów bitów stosunkowo łatwo jest zdefiniować hierarchię grup użytkowników pozwalającą zrealizować założenie dziedziczenia uprawnień.

    $logged_user = 1;
    $moderator = $logged_user | 2; // 3
    $sponsor = $logged_user | 4; // 5
    $admin = $moderator | $sponsor | 8; // 15

    Przetestujmy!

    $bob = $sponsor; // 5
     
    // Czy Bob ma dostęp do strefy zalogowanych użytkowników?
    ($for_logged & $bob) > 0 ? 'yes' : 'no';
    // wynikiem jest yes
     
    // Czy Bob ma dostęp do strefy moderatora?
    ($for_moderators & $bob) > 0 ? 'yes' : 'no';
    // wynikiem jest no
     
    // Czy Bob ma dostęp do strefy sponsorów?
    ($for_sponsors & $bob) > 0 ? 'yes' : 'no';
    // wynikiem jest yes
     
    // Czy Bob ma dostęp do strefy administratorów?
    ($for_administrators & $bob) > 0 ? 'yes' : 'no';
    // wynikiem jest no

    Operatory bitowe wyglądają tak samo w PHP, Pythonie czy MySQL-u. Można zapisać strefy, grupy i użytkowników bazie danych i większość operacji wykonać za pomocą zapytań sql-owych.

    CREATE TABLE `zones` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    `name` VARCHAR( 255 ) NOT NULL ,
    `level` TINYINT NOT NULL DEFAULT '0'
    ) ENGINE = MYISAM ;
     
    INSERT INTO `zones` (`id`, `name`, `level`) VALUES 
    (1, 'for_logged', '1'), (2, 'for_moderators', '2'), 
    (3, 'for_sponsors', '4'), (4, 'for_administrators', '8');
     
    CREATE TABLE `groups` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    `name` VARCHAR( 255 ) NOT NULL ,
    `perms` TINYINT NOT NULL DEFAULT '0'
    ) ENGINE = MYISAM ;
     
    INSERT INTO `groups` (`id`, `name`, `value`) VALUES 
    (1, 'logged_user', '1'), (2, 'moderator', '3'), 
    (3, 'sponsor', '5'), (4, 'administrator', '15');
     
    CREATE TABLE `users` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    `username` VARCHAR( 255 ) NOT NULL ,
    `groups_id` INT UNSIGNED NOT NULL
    ) ENGINE = MYISAM ;
     
    INSERT INTO `users` (`id`, `username`, `groups_id`) VALUES 
    ('1', 'Frank', '1'), ('2', 'Charlie', '2'), ('3', 'Bob', '3'), 
    ('4', 'Jon', '4'), ('5', 'Mary', '1');

    Poniżej użyłem podzapytań w celu pobrania listy użytkowników, którzy mają dostęp do strefy dla użytkowników zalogowanych, a następnie dla moderatorów.

    SELECT username FROM `users` AS u INNER JOIN groups AS g ON u.groups_id = g.id 
    WHERE g.perms & (SELECT z.level FROM zones AS z WHERE z.name = 'for_logged');
    // wynikiem jest lista: Frank, Mary, Charlie, Bob, Jon
     
    SELECT username FROM `users` AS u INNER JOIN groups AS g ON u.groups_id = g.id 
    WHERE g.perms & (SELECT z.level FROM zones AS z WHERE z.name = 'for_moderators');
    // wynikiem jest lista: Charlie, Jon

    Na zakończenie chciałbym zaprezentować mały testowy skrypt. Jeśli nie chce Ci się analizować kodu, skopiuj go, zapisz w pliku i uruchom, a wszystko stanie się jasne.

    /**
     * Rodzaje powiadomień
     */
    $notification_methods = array(
        'email' => 1,
        'internal_message' => 2,
        'notification' => 4,
        'wall' => 8
    );
     
     
    $nm = $notification_methods;
     
    /**
     * Rodzaje zdarzeń
     *
     * Do każdego rodzaju zdarzeń przypisane są dopuszczalne rodzaje powiadomień
     * np. o prywatnej wiadomości można powiadomić mailem lub za pośrednictwem
     * wewnętrznej wiadomości, ale nie wolno wyświetlić tej informacji na ścianie
     */ 
    $event_types = array(
        'newsletter'        => $nm['email'],
        'invite_to_friends' => $nm['internal_message'] | $nm['notification'],
        'priv_message'      => $nm['email'] | $nm['internal_message'],
        'image_comment'     => $nm['internal_message'] | $nm['notification'] | $nm['wall']
    );
     
     
    print '<form action="" method="post">';
    foreach ($event_types as $event_name => $avaliable_noti_methods) {
        echo '<p>';
        echo '<strong>'.$event_name.'</strong><br>';
        foreach ($notification_methods as $noti_name => $noti_val) {
            if ($avaliable_noti_methods & $noti_val) {
                $checked = (isset($_POST[$event_name.'-'.$noti_name]) ? 'checked="checked"' : '');
                echo '<input type="checkbox" name="'.$event_name.'-'.$noti_name.'[]" id="id_'.$event_name.'-'.$noti_name.'" '.$checked.'>';
                echo '<label for="id_'.$event_name.'-'.$noti_name.'">'.$noti_name.'</label><br>';
            }
        }
        echo '</p>';
    }
    echo '<p><input type="submit" name="ok" value="OK"></p>';
    echo '</form>';
     
    if (isset($_POST['ok'])) {
     
        $user_notification_settings = array_combine(array_keys($event_types), array_fill(0,4,0));
        foreach (array_keys($_POST) as $key) {
            $values = explode("-", $key);
            if (count($values) != 2) continue;
            list($event_name, $noti_name) = $values;
     
            if (!isset($user_notification_settings[$event_name])) {
                $user_notification_settings[$event_name] = $notification_methods[$noti_name];
            } else {
                $user_notification_settings[$event_name] |= $notification_methods[$noti_name];
            }
        }
     
        printf('<strong>Ustawienia wybrane przez użytkownika</strong>%s', print_r($user_notification_settings, true));
     
        print '<strong>Podsumowanie</strong><br>';
        foreach ($user_notification_settings as $event_name => $avaliable_noti_methods) {
            foreach ($notification_methods as $noti_name => $noti_val) {
                if ($avaliable_noti_methods & $noti_val) {
                    print $event_name.' - tak - '.$noti_name.'<br>';
                } else {
                    print $event_name.' - NIE - '.$noti_name.'<br>';
                }
            }
        }
    }
    Podziel się z innymi!

      Nice url – czyli przyjazne linki

      Podziel się z innymi!

        Jakiś czas temu pisałem o routingu w Kohana 3.1, w którym to wpisie stwierdziłem, że nie działają mi nazwane podwzorce (named subpattern) pomimo tego że mam wersję PHP wyższą niż minimalna wymagana 5.2.2. Trafiłem jednak na wpis nospora „Ładne url’e (nice url)” i postanowiłem mimo wszystko powrócić do tematu routingu i powalczyć z tymi wyrażeniami regularnymi.

        Może to kwestia konfiguracji komputera – gdyż eksperyment przeprowadzałem na innym kompie niż poprzednio ale tym razem okazało się, że jednak da się w PHP nadawać nazwy wycinkom wzorca.

        <?php
        // Przykładowe urle
        $urls = array(
            '/pl/news/list/1/',
            '/news/list/1/',
            '/news/list/',
        );
        // Ścieżka która pasuje do powyższych urli - parametry lang i page są opcjonalne
        $route = '%^/((?P<lang>\w{2})/)?(?P<controler>\w+)/(?P<method>\w+)/((?P<page>\d+)/)?$%';
         
        // Definicja wartości domyślnych parametrów opcjonalnych
        $defaults = array('lang' => 'pl', 'page' => 1);
         
        // Odczytanie ze ścieżki nazw parametrów
        preg_match_all('/P\<(\w+)\>/', $route, $matches, PREG_PATTERN_ORDER);
        $params = $matches[1];
         
        foreach ($urls as $url) {
            // Założyłem w przykładzie, że wszystkie urle pasują do ścieżki 
            // więc nie sprawdzam czy tak jest
            preg_match($route, $url, $matches);
            $vars = array();
            foreach ($params as $param) {
                // Oczytanie z urla wartości parametrów lub nadanie wartości domyślnych
                $vars[$param] = (isset($matches[$param]) && !empty($matches[$param])) 
                    ? $matches[$param] : $defaults[$param];
            }
            print $url."<br />"; print_r($vars);
        }
        ?>

        Wynikiem wywołania powyższego kodu jest

        /pl/news/list/1/

        Array
        (
            [lang] => pl
            [controler] => news
            [method] => list
            [page] => 1
        )
        

        /news/list/1/

        Array
        (
            [lang] => pl
            [controler] => news
            [method] => list
            [page] => 1
        )
        

        /news/list/

        Array
        (
            [lang] => pl
            [controler] => news
            [method] => list
            [page] => 1
        )
        

        Wyżej przytoczony przykład to tylko proof of concept, którego stworzenie nie zajęło mi więcej czasu niż wpisanie tych paru słów komentarza. Daje on jednak pojęcie jak łatwe i przyjemne jest stworzenie routingu w oparciu o nowe możliwości wyrażeń regularnych (nowe w PHP :)).

        Podziel się z innymi!

          Kohana 3 mod_rewrite i błąd „No input file specified”

          Podziel się z innymi!

            Stawiałem już projekty oparte na frameworku Kohana 3 na różnych serwerach. Jak dotąd zawsze działał mi plik .htaccess o treści:

            # Turn on URL rewriting
            RewriteEngine On
             
            # Installation directory
            RewriteBase /
             
            # Protect application and system files from being viewed
            RewriteRule ^(?:application|modules|system)\b - [F,L]
             
            # Allow any files or directories that exist to be displayed directly
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteCond %{REQUEST_FILENAME} !-d
             
            # Rewrite all other URLs to index.php/URL
            RewriteRule .* index.php/$0 [PT]

            (No ok mój plik htaccess jest bardziej robudowany, ale dla przejrzystości problemu podaję wersję minimalną – standardzik.)

            Dzisiaj jednak szybkie i przyjemne przerzucenie projektu na serwer docelowy zostało brutalnie zburzone przez niemiły komunikat „No input file specified”, który pokazywał mi się przy próbie przejścia na dowolną podstronę.

            Początkowo próbowałem użyć dyrektywy .htaccess-a

            Options -MultiViews

            Przy jakimś projekcie to mi kiedyś pomogło o ile dobrze kojarzę, ale tym razem nie.

            Problematyczna okazała się linijka:

            RewriteRule .* index.php/$0 [PT]

            , którą na wszelkich forach, blogach itd. proponowano zastąpić na kilka różnych sposobów

            RewriteRule .* index.php?$0 [PT,L,QSA]

            albo

            RewriteRule .* index.php [L]

            jednemu podobno zadziałało coś takiego

            RewriteRule .* index.php?kohana_uri=$0 [PT,L,QSA]

            Mnie zadziałało dowolne z powyższych rozwiązań, ale tylko połowicznie. To znaczy – komunikat błędu zniknął, ale za to bez względu na wybraną podstronę zawsze pokazywała mi się strona główna. Nienawidzę takich zagadek.

            Przeanalizowałem zawartość tablicy $_SERVER na moim serwerze testowym oraz produkcyjnym i wyszło mi, że na tym drugim brakuje PATH_INFO. Widać routing Kohany (przynajmniej w wersji 3.0) korzysta z tej wartości i bez niej zwyczajnie przestaje działać.

            Ostatecznym remedium na mój kłopot okazało się prostackie obejście

            $_SERVER['PATH_INFO'] = $_SERVER['REQUEST_URI'];

            Ja osobiście dodałem to w bootstrapie.

            Podziel się z innymi!

              Routing w Kohana 3.1

              Podziel się z innymi!

                Z frameworkiem Kohana pracuję już kilka lata. O ile Kohana w wersji 2 była przyjazna i w miarę dobrze udokumentowana o tyle 3-cie wydanie tego frameworka jest już mniej przyjazne. Nie zrozumcie mnie źle – ogólnie zmiany w architekturze i implementacji oceniam pozytywnie, ale ogromne braki w dokumentacji i ciągłe zmiany w kodzie mają zdecydowanie negatywny wpływ na przyjemność pracy z tym oprogramowaniem.

                Zabieram się właśnie za nowy projekt. Jest niewielki więc postanowiłem wypróbować Kohanę w najnowszej „stabilnej” wersji 3.1. Utworzyłem katalog dla projektu, przekopiowałem do niego pliki frameworka, utworzyłem plik hteaccess według wzoru i odpaliłem w przeglądarce adres.

                http://localhost/project/
                

                Uruchomił mi się instalator, który poinformował mnie, że muszę nadać prawa do zapisu odpowiednim katalogom. Pozostałe wymagane warunki miałem spełnione. Nie miałem PECL HTTP i cURL ale to opcjonalne zależności bez, których wszystko, a przynajmniej fundamenty Kohany powinny ruszyć więc je zignorowałem. Zgodnie z instrukcją zmieniłem nazwę plikowi install.php i…

                Otrzymałem komunikat błędu

                HTTP_Exception_404 [ 404 ]: The requested URL project/index was not found on this server.
                

                Popularny błąd 404 – not found – wydawałoby się i wszystko jasne. Tylko, że to pierwsze uruchomienie frameworka, w który jest wstępnie skonfigurowany, posiada domyślny kontroler „welcome” a w nim domyślną akcję „index” tak więc powinienem zobaczyć przyjazny napis „hello, world!”.

                Gdyby to było moje pierwsze zetknięcie z frameworkiem Kohana to pewnie po pierwszych 5 min. dałbym sobie spokój. Nie dość, że przy pierwszym uruchomieniu dostajemy exceptiona to jeszcze tak naprawdę nic nam on nie mówi o możliwościach jego rozwiązania. W końcu kontroler jest tam gdzie trzeba, klasa ma odpowiednią nazwę i jest metoda, która zgodnie z dokumentacją powinna zostać wywołana.

                Tymczasem przyczyną jest zła konfiguracja. W pliku /application/bootstrap.php mamy taki fragment kodu

                Kohana::init(array(
                    'base_url'   => '/',
                ));

                Parametr base url w moim przypadku powinien wyglądać tak

                Kohana::init(array(
                    'base_url'   => '/project/',
                ));

                Proste prawda? Tylko ktoś kto dopiero poznaje tę platformę się tego nie domyśli. Przyznaję się, że sam się trochę tego naszukałem, a to dlatego, że kiedyś już rozwiązałem ten problem kawałkiem uniwersalnego kodu i zdążyłem o nim zapomnieć. Tymczasem teraz stawiając nowy projekt na świeżutkiej Kohanie prosty problemik wrócił do mnie i trafił w głowę.

                $dirname = dirname($_SERVER['SCRIPT_NAME']);
                $base_url = preg_replace('@/+$@', '', $dirname=="\\"?'':$dirname).'/'; 
                 
                Kohana::init(array(
                    'base_url'   => $base_url,
                ));

                Dla purystów, którzy nie lubią zbędnych operacji i nie ufają zbytnio uniwersalności tego rozwiązania (w końcu było testowane tylko na serwerach z Apache 2) proponuję nieco zmodyfikowaną wersję

                Kohana::init(array(
                	'base_url'   => '/',
                ));
                 
                if (Kohana::$environment != Kohana::PRODUCTION) {
                    $dirname = dirname($_SERVER['SCRIPT_NAME']);
                    $base_url = preg_replace('@/+$@', '', $dirname=="\\"?'':$dirname).'/';
                    if (Kohana::$base_url != $base_url) {
                        throw new Kohana_Exception(sprintf('Perhaps you have a bad parameter set 
                            base_url in bootstrap.php. Most likely, the correct value should be 
                            "%s"', $base_url));
                    }
                }

                Routing w Kohana nie jest szczytem elegancji, wygody i elastyczności. W stosunku do routingu w Django, każdy tego typu system w PHP jest prymitywną próbą naśladownictwa. Wynika to w dużej mierze z ograniczeń wyrażeń regularnych w PHP, które aż do wersji 5.2.2 (PCRE 7.0) – przynajmniej według dokumentacji – nie obsługiwały nazwanych podwzorców (named subpattern).

                $str = 'foobar: 2008';
                preg_match('/(?P<name>\w+): (?P<digit>\d+)/', $str, $matches);
                print_r($matches);
                //Array
                //(
                //    [0] => foobar: 2008
                //    [name] => foobar
                //    [1] => foobar
                //    [digit] => 2008
                //    [2] => 2008
                //)

                Ja mam na swoim kompie PHP 5.3.5 (PCRE 8.12) i dalej mi to nie działa.

                Nie mniej routing w Kohana 3.1 wzbogacił się o możliwość definiowania ścieżek z użyciem funkcji lambda lub callback – w zależności od wersji PHP oczywiście. Daje to spore możliwości, których namiastkę spróbuję teraz zaprezentować.

                Standardowe ustawienia routingu w Kohana Framework wyglądają następująco.

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

                Powoduje to, że do strony głównej serwisu możemy się odwołać w trojaki sposób.

                http://domena.com/
                http://domena.com/welcome/
                http://domena.com/welcome/index
                

                Jest to problem SEO tylko jeśli w projekcie budujemy różne linki odwołujące się do strony głównej. Jeśli dojdzie do tego wielojęzyczność do adresu zostanie dodany dodatkowy parametr (no chyba, że strony w poszczególnych językach trzymane są na subdomenach). Wtedy strona główna będzie występować w różnych wersjach językowych, a dodatkowo strona główna w języku domyślnym będzie w wersji z oznaczeniem języka i bez.

                http://domena.com/
                http://domena.com/pl-pl/
                

                Dobrze byłoby zrobić aby strona główna w domyślnej wersji językowej była dostępna tylko pod adresem „/”. Natomiast próba wejścia przez uri „/pl-pl/” kończyła się przekierowaniem na „/”. Po wpisaniu w adres przeglądarki adresów „/welcome/” lub „/welcome/index” powinien być zgłoszony błąd 404.

                Aby to osiągnąć stworzyłem klasę ProcessRoute w pliku /application/classes/processroute.php

                <?php defined('SYSPATH') or die('No direct script access.');
                 
                class ProcessRoute {
                    public static function main_page($uri) {
                        $base_url = Kohana::$base_url;
                        if ($uri == I18n::$lang) {
                            header("Location: {$base_url}", true, 302); die();
                        } else if ($uri == '') {
                            if ($_SERVER["REQUEST_URI"] != $base_url) {
                                throw new HTTP_Exception_404('Unable to find a route to match the URI: :uri'
                                , array(':uri' => str_replace($base_url, '', $_SERVER["REQUEST_URI"])));
                            }
                            return array(
                		        'lang' => 'pl-pl',
                		        'directory' => '',
                		        'controller' => 'welcome',
                		        'action' => 'index',
                	        );
                        } else if (preg_match('/^[a-z]{2,2}-[a-z]{2,2}$/', $uri)) {
                            return array(
                		        'lang' => $uri,
                		        'directory' => '',
                		        'controller' => 'welcome',
                		        'action' => 'index',
                	        );
                        }
                        return false;
                    }
                }

                Myślę, że dodatkowego wyjaśnienia wymaga jedynie fragment

                if ($_SERVER["REQUEST_URI"] != $base_url) {
                    throw new HTTP_Exception_404('Unable to find a route to match the URI: :uri'
                    , array(':uri' => str_replace($base_url, '', $_SERVER["REQUEST_URI"])));
                }

                Otóż w przypadku wpisania w pole adresu przeglądarki urla „http://localhost/project/index/” wartość uri przekazana do metody ProcessRoute::main_page będzie miała pustą wartość „”. Jest to ewidentny bug i dlatego musiałem zastosować to nieeleganckie obejście.

                Aby podpiąć wyżej zaprezentowaną klasę do routingu należy w bootstrapie dodać

                /**
                 * Set the routes. Each route must have a minimum of a name, a URI and a set of
                 * defaults for the URI.
                 */
                Route::set('main', array('ProcessRoute', 'main_page'));

                Na stronie głównej budowa witryny internetowej się nie kończy. Podpięcie funkcji zwrotnej pod routing daje o wiele większe możliwości. Załóżmy, że chcę wyświetlać dane kontaktowe różne dla różnych wersji językowych.

                class Controller_Contact extends Controller {
                 
                    public function action_index()
                    {
                        $lang = Request::current()->param('lang');
                        if ($lang == 'en-en') {
                            $this->response->body('Contact');
                        } else {
                            $this->response->body('Kontakt');
                        }
                    }
                } // Contact

                Chciałbym aby polską wersję podpiąć pod adres „/pl-pl/kontakt”, a w wersję angielską pod „/en-en/contact”. W tym celu stworzyłem sobie plik konfiguracyjny /application/config/routes.php.

                return array
                (
                	'pl-pl' => array(
                	    'kontakt' => array(
                		    'controller' => 'contact',
                		    'action' => 'index',
                	    )
                	),
                	'en-en' => array(
                	    'contact' => array(
                		    'controller' => 'contact',
                		    'action' => 'index',
                	    )
                	)
                );

                Aby routing korzystał z tego pliku konfiguracyjnego do klasy ProcessRoute (w pliku /application/classes/processroute.php) dodałem statyczną metodę „static_pages”.

                class ProcessRoute {  
                    public static function main_page($uri) {
                        // ...
                    }
                    public static function static_pages($uri) {
                        // $lang = I18n::$lang;
                        if (preg_match('/^([a-z]{2,2}-[a-z]{2,2})\/(.*)/', $uri, $matches) && count($matches) == 3) {
                            $lang = $matches[1];
                            // if ($lang == I18n::$lang) return false;
                            $uri = $matches[2];
                        }
                        $routes = Kohana::config("routes")->as_array();
                        if (!isset($lang) || !isset($routes[$lang])) {
                            return false;
                        } else {
                            $routes = $routes[$lang];
                        }
                        if (array_key_exists($uri, $routes)) {
                            $route = $routes[$uri];
                            $route['lang'] = $lang;
                            return $route;
                        } 
                        return false;
                    }
                }

                Proszę zwrócić uwagę na wykomentowaną linię.

                $lang = I18n::$lang;

                Chciałem mieć pewność, żeby za wyjątkiem strony głównej na wszystkich podstronach był używany parametr wersji językowej. Mogę odkomentować wspomnianą linię i wtedy dane kontaktowe pojawią się zarówno po wybraniu adresu „/pl-pl/kontakt”, jak i „/kontakt”. Jeśli do tego odkomentuję jeszcze linię

                if ($lang == I18n::$lang) return false;

                wtedy z kolei dane kontaktowe w domyślnej wersji językowej będą dostępne jedynie pod adresem „/kontakt”.

                Zapomniałbym. W bootstrapie trzeba jeszcze wywołać metodę ProcessRoute::static_pages.

                Route::set('static_pages', array('ProcessRoute', 'static_pages'));

                Jak pokazują te proste przykłady routing w Kohana 3.1 zyskał na elastyczności. Odrobina pracy i można by z tego mechanizmu wycisnąć dużo więcej. Panel administracyjny można oprzeć o tradycyjny routing oparty na kontrolerach i akcjach z kolei część publiczną adresować z wykorzystaniem plików konfiguracyjnych lub też trzymać ścieżki w bazie danych.

                Podziel się z innymi!

                  Narzędzia ułatwiające sprawdzanie poprawności linków

                  Podziel się z innymi!

                    Jedną z najważniejszych rzeczy, o które trzeba zadbać podczas tworzenia stron internetowych jest poprawność adresów url. W tym konkretnym wpisie mam na myśli to aby po kliknięciu na dany odsyłacz serwer zwracał stronę ze statusem 200, albo przynajmniej przekierowanie, które ostatecznie trafi do treści docelowej. Wszelkiego rodzaju błędy (500 czy 404) są wysoce niepożądane tak dla użytkownika strony, jak i z punktu widzenia SEO.

                    Zadanie to mogą ułatwić narzędzia w stylu linkcheckera lub też dodatku do firefoxa Link Evaluator.

                    Linkchecker to przyjemne konsolowe narzędzie umożliwiające przetestowanie linków zarówno na stronach www udostępnionych w internecie, jak i tych zapisanych na lokalnym dysku. Istnieje też proste gui do niego, które ułatwia pracę ludziom niechętnie używającym terminala. Instalacja jest prosta w przypadku Ubuntu.

                    sudo apt-get install linkchecker linkchecker-gui

                    Oprócz sprawdzania statusów zwracanych przez wszystkie odsyłacze na stronie, linkchecker ma też szereg zaawansowanych opcji w tym sprawdzanie poprawności linków pod kątem zgodności ze specyfikacją html czy css, a także przeskanowanie zwracanej przez odsyłacz treści przez program antywirusowy ClamAV.

                    Link Evaluator to z kolei dodatek do przeglądarki Firefox. Ma on o wiele mniejszą funkcjonalność niż wspomniane wyżej oprogramowanie nie mniej jego zaletą jest łatwość użycia, wygoda i niezależność od systemu operacyjnego. Po wejściu na daną stronę WWW wystarczy kliknąć prawy przycisk, wybrać z menu kontekstowego „Evaluate Links” i poczekać aż wszystkie url-e na danej podstronie zostaną sprawdzone. Poprane adresy podświetlane są na zielono, z kolei na czerwono zaznaczane są nieprawidłowe linki. Wybranie z menu kontekstowego opcji „Link Evaluator Control Panel” otwiera okienko z opcjami dodatku, ale też z monitorem, w którym możemy obserwować przebieg pracy programu. Znalazłem kilka dodatków do firefoxa oferujących podobną funkcjonalność ale tylko Link Evaluator działał bezbłędnie i uwzględniał przekierowania.

                    Testowałem też kilka narzędzi online, ale żadne nie zwracało satysfakcjonujących wyników.

                    Podziel się z innymi!

                      Konwersja cdr, ai, eps do svg

                      Podziel się z innymi!

                        Nie jestem grafikiem, a do tego pracuję na Linuksie. Nie mam tej całej palety programów graficznych jak Photoshop, Adobe Illustrator czy CorelDRAW. Zazwyczaj mi to nie przeszkadza bo jeśli nawet dostaje jakiś layout do pocięcia to zlecam tę robotę innym. Do drobnych prac graficznych starcza mi Gimp, a niekiedy nawet edytor grafiki online Pixlr.

                        Niekiedy jednak trafiają mi się logotypy w formacie pliku AI (Adobe Illustrator Artwork), EPS (Encapsulated PostScript – Photoshop), CDR (CorelDRAW), a nawet PDF (Portable Document Format). Gimp poradzi sobie z plikami EPS choć nie najlepiej? Lepiej w tym wypadku sprawdzi się edytor grafiki wektorowej Inkscape, który zaimportuje pliki we wszystkich wyżej wspomnianych formatach i umożliwi ich zapisanie w domyślnym formacie SVG (Scalable Vector Graphics) lub też jako PNG albo XCF (natywny format Gimpa). Export do formatu XCF – co ważne – wykonywany jest z zachowaniem warstw. Inkscape umożliwia zapis pliku w wielu jeszcze innych formatach i jest dostępny w wersjach na Linuksa, Windowsa i Mac-a.

                        Jeśli mamy do czynienia z większą liczbą plików do przekonwertowania to wygodniejszym rozwiązaniem okazuje się skrypt uruchamiany z linii komend. Niestety funkcjonalność Inkscape-a jest ograniczona z poziomu konsoli dlatego warto sięgnąć do innych wyspecjalizowanych w tym względzie narzędzi takich jak uniwersalny translator grafiki wektorowej – Uniconvertor lub też translator plików Post Script i PDF do innych formatów grafiki wektorowej – Pstoedit. W Ubuntu oba programy dostępne są w repozytoriach i można je zainstalować za pośrednictwem apt-get -a

                        sudo apt-get install python-uniconvertor pstoedit

                        Ich użycie jest najprostsze z możliwych i ogranicza się do podania kolejno ścieżki do pliku źródłowego i ścieżki wynikowej.

                        uniconvertor logo.cdr logo.svg

                        i analogicznie dla pstoedit

                        pstoedit logo.eps logo.svg
                        Podziel się z innymi!

                          Kilka reflaksji na temat frameworków

                          Podziel się z innymi!

                            Ostatnio – po długiej przerwie – wszedłem na blog Zyxa wierząc, że znajdę tam coś ciekawego do przeczytania. Nie zawiodłem się. Znalazłem dwie recenzje najpopularniejszych w Polsce frameworków PHP tj. Symfony 1.4 okiem Zyxa i Zend Framework także okiem Zyxa.

                            Ostatnio przerzuciłem się na Pythona i Django, a wcześniej przez co najmniej dwa lata budowałem aplikacje w oparciu o Kohanę, jednak od czasu do czasu – w tak zwanym międzyczasie – próbowałem też coś sklecić na ZF i polubić Symfony dlatego też orientuję się przynajmniej pobieżnie w ich konstrukcji. Artykuły Zyxa i własne doświadczenie skłoniły mnie do kilku refleksji na temat ogólnej konstrukcji frameworków, sensu użycia ORM-ów a także systemów szablonów.

                            Architektura

                            Dobrze zaprojektowana struktura plików to podstawa porządnego frameworka. Każdy szanujący się projektant stara się utrzymać porządek w swoim projekcie i lubi bez zbędnego zastanawiania się wiedzieć gdzie ma czego szukać. Kohana umożliwia tworzenie modułów z których każdy może mieć swoje kontrolery, modele, widoki, pliki konfiguracyjne, a nawet biblioteki. Układ katalogów jest rozbudowany. Kaskadowość z jednej strony umożliwia elastyczność np. nadpisanie konfiguracji domyślnej, konfiguracją specyficzną dla danego modułu. (w Kohanej nie tylko konfigurację można nadpisać ale też „wymienić” klasy z zachowaniem ich dotychczasowej nazwy. Założenie to w PHP wyklucza proste użycie dziedziczenia dlatego też w Kohanie 2 było to zrobione po chamsku z użyciem evala. W wersji 3 tego frameworka mechanizm ten został bardziej elegancko zaimplementowany). Z drugiej strony przy bardziej rozbudowanych projektach człowiek zaczyna się gubić nie pamiętając niekiedy skąd się wzięła bieżąca wartość danej zmiennej konfiguracyjnej. Zaczyna się szukanie. Pół biedy, kiedy struktura katalogów jest płaska, ale jeśli musimy przeklikać się przez kilka poziomów zagłębień staje się to męczące.

                            Nie wiem kto na to wpadł, aby każdą klasę trzymać w osobnym pliku, nie wiem też kto wymyślił aby nazwa klasy odzwierciedlała położenie pliku w strukturze katalogów (patrz ZF, PEAR), ale doprowadziło to do powstania całej masy katalogów i podkatalogów i jeszcze większej liczby plików, z których niektóre np. zawierają jedynie jednolinijkową definicję wyjątku. W Django w poszczególnych app-sach znajdziemy z reguły pliki (__init__.py, views.py, models.py, urls.py, admin.py, tests.py) i to w 90% przypadków wystarcza. W Kohanie jeden moduł to kilka katalogów. Python jakoś obywa się bez autoloadera i kiedy czytam to co wyżej sam napisałem zaczynam rozumieć dlaczego. Na marginesie tylko wspomnę, że w związku z wprowadzeniem namespace-ów, sposób organizacji klas w plikach PHP się zmieni.

                            PHP w wersji piątej poszło wyraźnie w kierunku „magi”. Wszystkie poprzedzone podwójnym podkreśleniem metody są bardzo wygodnym rozwiązaniem i osobiście bardzo je lubię ale nadmiar czarów daje się we znaki w chwili kiedy zachodzi potrzeba prześledzenia procesów zachodzących w aplikacji. Debugowanie przesyconych „magią” klas jest znacznie utrudnione przede wszystkim przez niejednoznaczne komunikaty błędów. Dodatkową wadą użycia metod magicznych jest to, że dynamiczne settery i gettery nie będą podpowiadane przez żadne IDE typu NetBeans, czy Eclipse mimo w sumie dobrze zrealizowanej w nich funkcji podpowiadania składni.

                            Inną kwestią jest uniwersalność kodu. Klas Kohanej można używać tylko w ramach tego frameworka, z kolei biblioteki eZ Components lub Zend Frameworka można używać niezależnie lub w ramach zupełnie innej platformy. Sam wielokrotnie w projektach opartych na Kohanej sięgałem do wybranych komponentów Zend Frameworka. Klasy ZF są jak to zauważył Zyx dopracowane i przetestowane i jedyną ich wadą jest z reguły to, że są zbyt dobre. Zamiast w najprostszy sposób realizować banalną funkcjonalność, autorzy poszczególnych bibliotek prześcigają się w wymyślaniu wariantów zastosowań i sposobów użycia. Tak właśnie z noża powstał szwajcarski scyzoryk – fajny ale do smarowania chleba najlepszy jest zwykły nóż kuchenny. Ponoć w nowej wersji ZF ma to ulec zmianie.

                            ORM

                            Symfony zniechęciło mnie do siebie przede wszystkim ORM-em. Do Propela nawet nie startowałem, natomiast z Doctrine-m walczyłem jakiś czas. Niestety na etapie kiedy przeprowadzałem moje eksperymenty Doctrine było mocno niedopracowane w związku z czym wielokrotnie wzbudzało to moją irytacją. Z tego co pamiętam nie mogłem nawet dowolnie wskazać miejsca generowania modeli tylko było to z góry narzucone. Dlatego między innymi zarzuciłem pomysł „dokooptowania” Doctrine do Kohany.

                            Na tę chwilę o wiele większe doświadczenie mam z ORM-em Django uważanym za wzorcowy. Jest on o wiele bardziej dopracowany niż wyżej wspomniane PHP-owe odpowiedniki w związku z czym da się z nim w miarę sprawnie pracować. Mimo to pozostaję sceptyczny w kwestii użycia tego typu narzędzi. Opanowanie django-wego ORM-a kosztowało mnie sporo czasu, a do tego nie wyobrażam sobie aby można było efektywnie go używać nie znając wcześniej SQL-a. Przy prostych konstrukcjach jest miło i przyjemnie przy bardziej skomplikowanych użycie obiektów rzutuje przede wszystkim na wydajność, do czego przyczyniają się w dużej mierze bajery typu „lazy loading”. Zmiana ORM-owej konstrukcji w celu optymalizacji i tak poprzedzana jest napisaniem zapytania w SQL więc człowiek nie ma żadnej korzyści z użycia ORM-a, która by rekompensowała nakłady poniesione w celu jego poznania.

                            O wiele bardziej przemawiają do mnie składacze zapytań SQL takie jak Zend_Db – choć jak większość bibliotek Zenda jest ona przedobrzona. Mając prostą klasę do konstruowania zapytań SQL – taką która ułatwia, nie ogranicza i bynajmniej nie zmusza do uczenia się zamienników w stylu „annotate” zamiast „group by”, wspartą wzorcem projektowym DAO można stworzyć prosty w utrzymaniu, debugowaniu, elastyczny, wcale nie pracochłonny i przede wszystkim odseparowany od warstwy logiki i widoku mechanizm dostępu do bazy danych.

                            ORM-y mogą być w moim mniemaniu jedynie dodatkami używanymi np. do automatycznego generowania backedu (czytaj panelu administracyjnego), ale w żadnym wypadku nie powinny być jedynym ani nawet głównym sposobem pracy z danymi pobieranymi z bazy danych.

                            Szablony

                            Symfony a także Zend Framework, jak równierz Kohana używają natywnych szablonów PHP. Podobnie jak Zyx zgadzam się, że można znaleźć lepszą alternatywę dla warstwy widoku. W odróżnieniu jednak od niego nie stawiałbym na Open Power Template’a, które uważam za trudne i pracochłonne w użyciu (z samej swojej xml-owej natury), a do tego mniej elastyczne i intuicyjne niż kontestowane przez niego Smarty.

                            System szablonów Smarty w wersji 3 zostało wzbogacone o kilka ciekawych funkcjonalności jak np. dziedziczenie szablonów i możliwość nadpisywania bloków kodu znane z szablonów Django. W przeciwieństwie do ORM-ów uważam, że warto potrudzić się troszkę ze składnią Smarty i pogodzić się z narzutem na wydajność bo w zamian można zyskać szereg usprawnień jakich jesteśmy pozbawieni w przypadku użycia gołych szablonów PHP. Wspomnę tylko wygodniejszą składnię czy zaimplementowany cache, ale też zwiększone bezpieczeństwo wymuszone ograniczoną dostępnością funkcji PHP wewnątrz szablonu czy też encapsulacją zmiennych. Smarty 3 umożliwia też użycie natywnych szablonów PHP ale jest to alternatywa nie ograniczenie.

                            Do niedawna pracowałem jeszcze z Django w wersji 9.6, w której szablony w stosunku do wersji 1.2 były mocno ograniczone. Obecne szablony Django w wersji 1.x przypominają Smarty (albo na odwrót jak ktoś woli) – z tym, że Smarty 3 są po prostu lepsze. Jestem fanem składni i możliwości języka Python mimo to na polu szablonów PHP u mnie wygrywa.

                            Reasumując

                            We wpisie tym nie dążyłem bynajmniej do porównania frameworków między sobą, a już na pewno się do porównywania rozwiązań stosowanych w języku PHP czy Python. Poruszyłem trzy zagadnienia, które w mniejszym lub większym stopniu dotyczą wszystkich z wyżej wymienionych platform. Wspomniałem o tym, że jestem zwolennikiem rozwiązań uniwersalnych ale elastyczność nie może być realizowana kosztem przejrzystości i prostoty. Dałem też do zrozumienia, że mapowanie na siłę relacyjnej bazy danych do postaci obiektów jest raczej wyrazem fundamentalizmu ideologicznego niż pragmatycznym działaniem. Z kolei w użyci systemów szablonów dostrzegłem wiele zalet.

                            Podziel się z innymi!

                              Liczba mnoga (msgid_plural) w plikach „po” gettext-a w django

                              Podziel się z innymi!

                                Django jest rozbudowanym frameworkiem przewidzianym m.in. do tworzenia wielojęzykowych serwisów. Wykonanie strony w kilku wersjach językowych wymaga uwzględnienia wielu zagadnień, takich jak formaty daty, czasu, waluty a nawet oznaczania części dziesiętnych w liczbach. W poszczególnych językach różny jest porządek sortowania choćby z uwagi na znaki narodowe wzbogacone o akcenty czy ogonki – jak w naszym rodzimym, polskim języku. Sporym wyzwaniem jest też gramatyka w tym szyk zdania, przypadki i liczba mnoga.

                                Django jest przygotowane do pracy z gettext-em – oprogramowaniem do tworzenia tłumaczeń. To profesjonalne narzędzie uzupełnione wieloplatformowym edytorem plików poedit służącym do edycji plików „.po” (rozszerzenie plików źródłowych gettext-a) i jednocześnie kompilującym owe pliki do formatu binarnego „.mo” przyspiesza i ułatwia umiędzynaradawianie oprogramowania tworzonego w przeróżnych językach programowania. Ja z powodzeniem używałem gettexta w aplikacjach PHP i Python.

                                Standardowo treść pliku „.po” składa się z szeregu par zmiennych msgid i msgstr, z których pierwsza zawiera treść komunikatu oryginalnego, a druga tłumaczenie.

                                msgid "Komunikat oryginalny"
                                msgstr "Tłumaczenie komunikatu"
                                

                                Istnieją jednak komunikaty wymagające uwzględnienia liczby mnogiej. Na przykład zawierające zmienną wypełnianą dynamicznie.

                                msgid "We offer %(num_homes)d homes for sale "
                                

                                Format gettext-a uwzględnia wiele aspektów poszczególnych języków narodowych w tym m.in. różny sposób tworzenia liczby mnogiej. W przypadku języka polskiego jest to sprawa o tyle skomplikowana, że jeden rzeczownik może mieć kilka form liczby mnogiej zależnie od liczby elementów.

                                Mamy w ofercie 2 domy na sprzedaż
                                Mamy w ofercie 5 domów na sprzedaż
                                

                                W takim wypadku wpis w pliku „.po” może wyglądać następująco. Uwzględniona jest wersja pojedyncza oraz wersje mnogie.

                                msgid "Please correct the error below."
                                msgid_plural "Please correct %(num_errors)d errors below."
                                msgstr[0] "Proszę poprawić poniższy błąd."
                                msgstr[1] "Proszę poprawić %(num_errors)d poniższe błędy."
                                msgstr[2] "Proszę poprawić %(num_errors)d poniższych błędów."
                                

                                Aby gettext wiedział jaką logiką ma się posługiwać przy konstruowaniu liczby mnogiej należy go o tym poinformować. Oprócz wyżej wspomnianych zmiennych zawierających etykiety komunikatów oraz komunikaty właściwe, w plikach „.po” znajdują się też komentarze informujące m.in o tym w jakim pliku i w której linii dany komunikat się znajduje. Są też tzw. nagłówki stanowiące swego rodzaju metadane plików tłumaczeń.

                                Przykładowe nagłówki:

                                "Content-Type: text/plain; charset=UTF-8\n"
                                "Content-Transfer-Encoding: 8bit\n"
                                "X-Generator: Lokalize 0.2\n"
                                

                                Większość tychże nagłówków najlepiej pozostawić w postaci oryginalnej, jednak nagłówek informujący o formacie liczby mnogiej może wymagać dodania lub też zmiany.

                                "Plural-Forms: nplurals=2; plural=(n != 1);\n"
                                

                                Taki nagłówek jest prawidłowy m.in dla języka angielskiego, niemieckiego, hiszpańskiego itd. Nagłówek dla wersji polskiej jest już bardziej rozbudowany.

                                Plural-Forms: nplurals=3; \
                                              plural=n==1 ? 0 : \
                                                     n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
                                

                                Szczegółową listę nagłówków dla różnych języków i grup językowych można znaleźć w dokumentacji getext-a w części poświęconej formom liczby mnogiej.

                                Na zakończenie dodam jedynie, że brak tego nagłówka w pliku „.po” przy próbie jego zapisu kończy się komunikatem o błędzie krytycznym. W moim przypadku jeśli edytowany był wpis przewidujący liczby mnogie to usunięciu uległy wszystkie zmienne msgstr[0], msgstr[1] itd. znajdujące się pod msgid_plural owych wpisów. Plik był mimo to zapisywany co przy próbie ponownego otwarcia pliku „.po” kończyło się komunikatem o uszkodzeniu pliku. Można to było łatwo naprawić dodają wyżej wspomniane zmienne, jednak problem ostatecznie został rozwiązany po dodaniu odpowiedniego nagłówka.

                                Podziel się z innymi!

                                  VAT o 1% – kolejny pretekst do podniesienia cen

                                  Podziel się z innymi!

                                    Poszedłem dzisiaj do fryzjera. Przeciętny człowiek stara się zadbać o swoją głowę jeszcze przed świętami, a już na pewno przed Sylwestrem. Ja nie lubię kolejek i wolę to zrobić na spokojnie już w nowym roku, ale dzisiaj trudno mi było opanować wzburzenie. „To tylko 2 zł więcej” – usłyszałem.

                                    No dobrze. Kwotowo to nie wiele, ale zważywszy, że poprzednio za obcięcie „głowy” płaciłem 17 zł to 2 zł więcej stanowi blisko 12%-tową podwyżkę. W grudniu znajomy kupił samochód za 50 tys. zł – gdyby tak samo wzrosły ceny samochodów to dzisiaj musiałby dać za niego 56 tys. zł (no precyzyjnie rzecz biorąc to 50 tys – 22% VAT + 12% podwyżki + 23% VAT to daje ok 56460 zł)

                                    I teraz wziąwszy pod uwagę, że każdy sklepikarz, taksówkarz, szewc, każda stacja benzynowa, hipermarket czy Kowalski sprzedający buraki na straganie zrobił z dniem 1 stycznia to samo, to się okazuje, że zarabiam najwyżej 88% tego co w zeszłym roku.

                                    Wzrost VAT-u o 1% spowodował kilku a nawet kilkunasto procentowy wzrost cen, a im tańszy produkt tym wzrost większy bo dajmy na to bułka za 2 zł po 12% podwyżce będzie już kosztować 2,24, ale kto da cenę 2,24? Minimum 2,30 a to już jest 15% więcej.

                                    – Jeśli jesteś pracownikiem – to choć Twój pracodawca podniósł ceny na wszystkie produkty – Ty nie licz na wyższą pensję.
                                    – Jeśli jesteś płatnikiem VAT i kupujesz produkt, od którego możesz odliczyć sobie podatek VAT to w 90% przypadków i tak cena netto będzie wyższa od tej z zeszłego roku.
                                    – Jeśli nie możesz odliczyć VAT-u to jesteś krową, którą wszyscy doją.

                                    Większość z nas jest dojną krową. Mając działalność gospodarczą jesteś trochę mniej mleczną krasulą bo sam możesz podnieść ceny na swoje usługi i parę rachunków wrzucić w koszty, ale i tak dopłacasz do tego biznesu. Natomiast zarobią na tym Ci, którym i tak dotąd niczego nie brakowało.

                                    Czy zarobi na tym Państwo? Cuż 23% to więcej od 22%, a 23% od droższych produktów i usług to dużo więcej niż 22% od tańszych produktów i usług. Z drugiej jednak strony budżet gromadzi się po to aby go wydać więc może się okazać, że te wysokie 23% ma mniejszą siłę nabywczą niż tamto słabe 22%.

                                    Podziel się z innymi!

                                      Posted in Priv by Zbigniew Heintze

                                      Wydobywanie tekstów i grafik z plików PDF

                                      Podziel się z innymi!

                                        Bywa że klient przysyła Ci treści do umieszczenia na stronie z krótkim komentarzem – „Tak to sobie wyobrażam”. Otwierasz maila i widzisz plik PDF. Są tam pięknie ułożone teksty okraszone kolorowymi fotografiami, wszystko elegancko poukładane i skomponowane, a Ty drapiesz się w głowę i nic nie mówisz bo szkoda słów. Jeśli jesteś webdeveloperem, specjalistą od cięcia i stylowania to szczerze Ci współczuję. Jeśli jednak Twoim jedynym zadaniem jest wydobycie z tego PDF-a wszystkich tekstów oraz grafik to jest nadzieja.

                                        Klikacze lub marzyciele często patrzący w „okna” mogą skorzystać z jednej z rad proponowanych w artykule How to Extract Text from a PDF document lub How Can I Get Text or Images Out of a PDF File?. Alternatywą jest użycie konsoli i wierzcie mi w tym przypadku jest to o wiele wygodniejsze i szybsze rozwiązanie.

                                        Do wyłuskania tekstów używamy programu „pdftotext” będącego częścią programu „xpdf

                                        pdftotext ./dokument.pdf

                                        Gdyby były problemy z polskimi literami warto zapoznać się z opcją „enc”:

                                        pdftotext -enc Latin2 dokument.pdf

                                        albo

                                        pdftotext -enc UTF-8 dokument.pdf

                                        Z obrazkami robimy identycznie tylko, że używając programu „pdfimages” także będącego konsolowym narzędziem „xpdf-a”

                                        pdfimages ./dokument.pdf  przedrostek_obrazka

                                        Program „pdfimages” zapisuje obrazki w formacie plików „ppm” więc trzeba je jeszcze przekonwertować do jpg-ów np. przy pomocy programu „pnmtojpeg” będącego częścią większego pakietu programów graficznych „Netpbm„.

                                        for pic in *.ppm 
                                        do 
                                            pnmtojpeg "${pic}" > "${pic/%ppm/jpg}" 
                                        done

                                        O ile programy „pdftotext” jak i „pdfimages” miałem już w systemie to „pnmtojpeg” trzeba było doinstalować i kiedy już to zrobiłem i przetestowałem przypomniałem sobie, że mam przecież zainstalowanego „ImageMagick-a„, którego użycie okazało się jeszcze prostsze.

                                        mogrify -format jpg *.ppm

                                        Pliki wynikowe uzyskane po zastosowaniu „mogrify” są większe co sugeruje mniejszą stratę na jakości, poza tym jak się ma „ImageMagick-a” to można z tymi plikami zrobić przy okazji dużo więcej np. automatycznie przeskalować, utworzyć miniatury, dodać ramki czy co nam tam jeszcze przyjdzie do głowy. Tworząc skrypt, który wszystkie wyżej przytoczone komendy zbiera w jedną zdecydowałem się właśnie na „mogrify”.

                                        Plik extractpdf.sh

                                        #!/bin/sh
                                         
                                        if [ $# -lt 1 ]; then
                                            echo "Usage: `basename $0` file.pdf"
                                            exit 1;
                                        fi
                                         
                                        PDF=$1
                                        FILE_NAME=`basename ${PDF%.*}`
                                        TEMP_DIR="`dirname $PDF`/${FILE_NAME}"
                                         
                                        if [ ! -e $TEMP_DIR ]; then
                                            mkdir -p $TEMP_DIR;
                                        fi
                                        pdftotext $PDF "${TEMP_DIR}/${FILE_NAME}.txt"
                                         
                                        pdfimages $PDF "${TEMP_DIR}/${FILE_NAME}"
                                         
                                        mogrify -format jpg ${TEMP_DIR}/*.ppm
                                        find ${TEMP_DIR}/ -name "*.ppm" -exec rm {} \;

                                        Skrypt oszczędza masę pisania poza tym jest uniwersalny więc nadaje się do wielokrotnego użytku. Teraz wystarczy nadać mu prawa do wykonywania

                                        chmod +x ./extractpdf.sh

                                        i cała praca to wywołanie skryptu z podaniem ścieżki do pliku pdf w parametrze

                                        ./extractpdf.sh ./dokument.pdf

                                        Można pokusić się o rozbudowę tego skryptu o sprawdzanie czy wymagane programy są zainstalowane oraz zwiększyć funkcjonalność poprzez próbę użycia „pnmtojpeg” w przypadku jeśli „ImageMagick” nie jest zainstalowany.

                                        Plik extractpdf2.sh

                                        #!/bin/bash
                                         
                                        if [ $# -lt 1 ]; then
                                            echo "Usage: `basename $0` file.pdf"
                                        	exit 1;
                                        fi
                                         
                                        type -P pdftotext &>/dev/null || { echo "I require pdftotext but it's not installed.  Aborting." >&2; exit 1; }
                                         
                                        type -P pdfimages &>/dev/null || { echo "I require pdfimages but it's not installed.  Aborting." >&2; exit 1; }
                                         
                                        if type -P mogrify >/dev/null; then
                                            CONVERTER="mogrify"
                                        else
                                            type -P pnmtojpeg &>/dev/null || { echo "I require the mogrify or pnmtojpeg but none of them is not installed.  Aborting." >&2; exit 1; } 
                                            CONVERTER="pnmtojpeg"
                                        fi
                                         
                                        PDF=$1
                                        FILE_NAME=`basename ${PDF%.*}`
                                        TEMP_DIR="`dirname $PDF`/${FILE_NAME}"
                                         
                                        if [ ! -e $TEMP_DIR ]; then
                                            mkdir -p $TEMP_DIR;
                                        fi
                                         
                                        pdftotext $PDF "${TEMP_DIR}/${FILE_NAME}.txt"
                                         
                                        pdfimages $PDF "${TEMP_DIR}/${FILE_NAME}"
                                         
                                        if [ $CONVERTER = 'mogrify' ]; then
                                            mogrify -format jpg ${TEMP_DIR}/*.ppm
                                            find ${TEMP_DIR}/ -name "*.ppm" -exec rm {} \;
                                        else
                                            for PIC in ${TEMP_DIR}/*.ppm
                                            do
                                                pnmtojpeg "${PIC}" > "${PIC/%ppm/jpg}"
                                                rm $PIC
                                            done
                                        fi

                                        UWAGA!!! Użycie „type -P” wymaga skorzystania konkretnie z powłoki „bash” gdyż w „sh” program „type” nie ma opcji „-P” i traktuje ją jak ścieżkę do pliku co kończy się komunikatem błędu (-P: not found). Dlatego pomimo, że plik dalej nazywa się extractpdf.sh to zamiast „#!/bin/sh” należy wpisać „#!/bin/bash”.

                                        To oczywiście nie wyczerpuje naszych możliwości. Zaproponowane przeze mnie narzędzie to niezbędne minimum. Stworzenie tego skryptu kosztowało mnie trochę czasu ale jest to praca jednorazowa. Podobnie miałem ze skryptem do generowania miniatur za to obecnie stale mam go w swoim arsenale i wierzcie mi, że suma sumarum zaoszczędził mi już naprawdę wiele czasu.

                                        Podziel się z innymi!

                                          Posted in Bash by Zbigniew Heintze · Tag: