Jakiś czas temu, na forum 4programmers.net przewinął się temat skracania tekstu. Temat o tyle skomplikowany, że chodziło o skrócenie głównej treści artykułu, zostawiając w nim wszystkie znaczniki. Problemy tutaj są dwa (podstawowe):

  1. dbanie o nieucinanie znaczników, żeby każdy rozpoczęty był we właściwej kolejności zakończony
  2. skracanie tekstu tak, żeby długość widocznego tekstu nie była skracana o tekst znaczników

W przytoczonym wątku problemem są tagi BBCode, które mogą mieć różne formy:

[tag]treść[/tag]
[tag=coś]treść[/tag]
[tag]

Trzeba pamiętać (na co mi zwrócono uwagę w przytoczonym wątku), że w takim np. tagu [url] może być zawarty adres URL, który nie może zostać ucięty. Skrócony może zostać tylko i wyłącznie tekst zawarty między znacznikiem rozpoczynającym a kończącym.

Sposób, na jaki wpadłem, opierał się na rozbiciu tekstu na poszczególne części za pomoca preg_split, i późniejszej obróbki powstałej w ten sposób tablicy. Po ominięciu pewnych trudności z kombinacjami takimi jak:

[url][img=aa]coś coś[/img][/url]

skonstruowałem funkcję, której można użyć do tego celu.

Jeśli komuś mogłaby się ona przydać (a myślę że więcej osób może mieć tego typu problem), to zapraszam do skorzystania z gotowca :).

W przypadku, gdy ktoś nie korzysta z BBCode’u, można użyć do tego celu parsera XML (zakładam korzystanie z XHTML raczej, niż HTML). Dodałem podobną funkcjonalność do mojej klasy mParser, która docelowo służy do filtrowania w tekście pobranym od użytkownika niebezpiecznych danych.


Kilka dni temu, Mariusz Jakubowski aka kg zauważył jeden, dość poważny brak w moich testach dotyczących szybkości foreach() i while(), mianowicie brak w odpowiednim miejscu reset()-owania tablicy. Dość diametralnie zmieniło to wyniki…

Dla porównania: stara wersja testu (bez resetowania) oraz nowa wersja testu (aby zobaczyć jak jest testowana całość, proponuje dodać do powyższych linków literkę ’s’). Kod używanej przeze mnie tablicy jest pod adresem: urzenia.net/tests/foreach:while_array.phps.

Nie mam na razie jak sprawdzić dla PHP w wersji 4, ale dla wersji jaka jest na serwerze (czyli 5.1.2) wersje foreach() jest zawsze odrobinę szybsza, a także przyjemniejsza w odczycie :)

Podsumowując: wyniki testu z 5.06.2005 r uznaję za niebyłe, radośnie wracając wszelką chwałę używanemu przeze mnie namiętnie foreach() ;)

PS. kg: dzięki!


Stanąłem dziś przed takim dylematem: chciałem utworzyć listę obiektów (każdy obiekt to konkretny wpis w bazie danych). No OK, samo utworzenie listy to niewielki problem. Problemem jest to, że w całej aplikacji używam dość zaawansowanych “ułatwiaczy”: przeładowywanie __isset(), __unset(), __set() i __get(). Np. mam obiekt $p, instancję klasy Post. Ma on różne własności, np tytuł (Post::$title), treść (Post::$body). Dostęp do nich jest za pomocą przeładowanych metod __set() i __get(), co pozwala mi na zautomatyzowanie kilku rzeczy - jak na przykład niewidoczną dla programisty zamianę znaków nowej linii na HTMLowe znaczniki <br />. Ale ja nie o tym miałem…

Chciałem zachować naturalność rozwiązań także w klasie agregującej obiekty Post. Czyli potraktować de facto obiektu agregatu (który ma kilka zadań, jak np utworzenie tej listy obiektów) jako tablicy. Tworząc instancję klasy, chcę mu zapodać: utwórz listę wpisów z kategorii o ID = 2, obiektów ma być 30. No i z tego korzystać. Mogę oczywiście dać sobie dostęp do jednej z własności tegoż obiektu-agregatu, która będzie tablicą i zawierać w sobie będzie wszystkie pobrane wpisy. Ale czy wspominałem coś o naturalności rozwiązań? :)

PHP 5 dostarcza nam coś, co nazywa się SPL, czyli Standard PHP Library. Jedną z klas (a konkretnie intefejsów) modułu SPL jest ArrayAccess.

Aby tego użyć, należy zadeklarować klasę jako implementującą z tego interfejsu:

class Aggregate implements ArrayAccess {}

A w niej trzeba zaimplementować 4 metody: offsetExists($offset), offsetGet($offset), offsetSet($offset, $value), offsetUnset($offset).

Przykład tego, jak powinna wyglądać cała klasa:

class Aggregate implements ArrayAccess
{
    private $data = array();

    public function __construct($array=null)
    {
        if (!is_null($array)) {
            $this->data = $array;
        }
    }

    public function offsetExists($offset)
    {
        return array_key_exists($offset, $this->data);
    }

    public function offsetGet($offset)
    {
        return $this->data[$offset];
    }

    public function offsetSet($offset, $value)
    {
        $this->data[$offset] = $value;
    }

    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }
}

Do tego przydałby się prosty sposób na sprawdzenie liczby obiektów które są na liście (kazaliśmy pobrać 30, ale to jest ilość maksymalna - w bazie danych może być ich mniej). Wykonamy to poprzez interfejs Countable i metodę count(). Nagłówek naszej klasy zmieniamy na taki:

class Aggregate implements ArrayAccess, Countable

I dodajemy metodę count():

    public function count()
    {
        return count($this->data);
    }

Żeby poczuć się jakbyśmy pracowali z prawdziwą tablicą, brakuje nam jeszcze możliwości iteracji po poszczególnych elementach tablicy. Do tego trzeba zaimplementować interfejs Iterator. Do listy implementowanych interfejsów dodajemy Iterator:

class Aggregate implements ArrayAccess, Countable, Iterator

I implementujemy jego metody:

    public function current()
    {
        return current($this->data);
    }

    public function key()
    {
        return key($this->data);
    }

    public function next()
    {
        return next($this->data);
    }

    public function rewind()
    {
        return reset($this->data);
    }

    public function valid()
    {
        return current($this->data);
    }

Od tego momentu możemy każdą instancję klasy Aggregate traktować jak tablicę. Pobieramy dowolny element tak jak z normalnej tablicy: $aggregate[2], tak samo zmieniać jego zawartosć: $aggregate[2] = ’something’;, iterować za pomocą foreach() {}, usuwać elementy za pomocą unset() etc. Jedyne czego nie osiągnęłem, ale też specjalnie nie próbowałem, to dodawać elementów za pomocą notacji []. Ale myślę że w większości zastosowań nie jest to potrzebne.

Cała, działająca klasa wraz z przykładem użycia: urzenia.net/wp-content/class_aggregate.php.

UPDATE:
Udoskonalona wersja klasy, odporna na błąd z metody valid (), a także pozwalająca na dodawanie kolejnych elementów tablicy PHPowym idiomem $aggregate[] = ‘wartosc’ znajduje się pod adresem: urzenia.net/wp-content/class_aggregate_new.php.


Mało kto wie, że funkcji z pakietu ob_* można użyć na przykład do konwersji z jednego kodowania do drugiego (np. strona de facto zapisana w ISO-8859-2 może pójść do przeglądarki jako UTF-8), albo do automatycznej kompresji za pomocą gzip, o ile przeglądarka wspiera tą metodę.

Do zmiany strony kodowej, oprócz funkcji z pakietu ob_* potrzebujemy rozszerzenia albo iconv, albo mb. Najpierw trzeba zdefiniować, z jakiego kodowania chcemy “tłumaczyć”:

iconv_set_encoding('internal_encoding', $enc_from); //jeśli korzystamy z iconv
mb_internal_encoding($enc_from); //jeśli korzystamy z mbstring

gdzie $enc_from może być np. “ISO-8859-2″ (lista obsługiwanych kodowań na stronie PHP (dla mbstring) lub na stronie biblioteki iconv (dla iconv).

Nastepnym krokiem jest zdefiniowanie kodowania wyjściowego:

iconv_set_encoding('output_encoding', $enc_to); //dla iconv
mb_http_output($enc_to); //dla mbstring

gdzie $enc_to w naszym przykładzie to “UTF-8″.

Teraz pozostaje tylko na początku strony wstawić instrukcję uruchamiającą buforowanie wyjścia:

ob_start('ob_iconv_handler'); //w przypadku iconv
ob_start('mb_output_handler'); //gdy używamy mbstring

I już możemy się cieszyć zmienionym kodowaniem danej strony :)

Uruchomienie kompresji jest równie proste, trzeba tylko pamiętać aby był załadowany moduł zlib. Samo uruchomienie kompresji to podanie funkcji ob_start() odpowiedniego parametru:

ob_start('ob_gzhandler');

Za pomocą instrukcji ini_set() możemy ustawić także stopień kompresji:

ini_set('zlib.compresss_level', $comp_level);

gdzie $comp_level jest to liczba całkowita z zakresu od 0 do 9 (domyślnie 6).

Pozostaje jeszcze tylko jeden problem: jak połączyć razem konwersję z jednego kodowania na drugie wraz z kompresją? Proszę:

ob_start(array('ob_gzhandler', 'ob_iconv_handler'));

Ważna powyżej jest kolejność parametrów podawanych w tablicy: gdy podasz je w odwrotnej kolejności, nie będzie działać kompresja.


Właśnie przeglądam sobie NEWS ze snapshota 200605310430 PHP 5.2. Wygląda, że będzie miał przynajmniej dwie fajne rzeczy:

  • Changed __toString to be called whereever applicable. Rozumiem przez to że __toString() będzie wywoływane także np przy konkatenacji stringów - jak na razie wynik __toString() można obejrzeć tylko przy bezpośredniej próbie wyświetlenia obiektu, co jest raczej mało użyteczne…
  • Added RegExIterator and RecursiveRegExIterator - to może być czasem niezwykle przydatne.

Do tego dojdzie nowy rodzaj błędu: E_RECOVERABLE_ERROR, ale jak na razie nie widze żadnego opisu na jego temat. Tak czy inaczej, ja nie mogę się doczekać PHP6, bo wkurza mnie kiepska obsługa UTF-8 ;)


Zainspirowany klasą autorstwa hwao, ruszyłem dziś do boju. Tak na szybko stworzyłem własną klasę do pobierania danych od usera, nazwaną getGPC. Można ją tymczasowo znaleźć pod adresem http://urzenia.net/wp-content/getgpc (nie klikalny, jako że ten adres niedługo ulegnie zmianie).

UPDATE
Klasa sporo sie zmieniła dziś nad ranem, a przed chwilą zmieniło się też miejsce przebywania klasy. Aktualna specyfikacja i instrukcja znajduje się na świeżo otwartym repo.urzenia.net, a źródła klasy są pod adresem repo.urzenia.net/files/.

Poniższe informacje można uznać za deprecated

Sposób użycia:

$r = getGPC::init([$source][, $sqlEscapeFun]); //użyty singletone
echo $r->getString($name[, $default][, $source][, $html][, $sql][, $syntax]);

Gdzie:

$source = getGPC::AUTO
pochodzenie zmiennej:

  • getGPC::POST - jeśli z $_POST
  • getGPC::GET
  • getGPC::COOKIE
  • getGPC::REQUEST
  • getGPC::AUTO - wtedy przeszuka kolejno $_POST, $_COOKIE, $_GET i $_REQUEST, i zwróci pierwszą znalezoną wartość
$sqlEscapeFun = ‘mysql_real_escape_string’
funkcja jaka zostanie użyta do escape’owania danych podczas przygotowania danych do wrzucenia do sql. Jeśli chcemy użyć takiego np. PDO::quote(), robimy to tak:$getgpc_instance->sqlEscapeFun=array($pdo_instance, 'quote');
$name
nazwa szukanej zmiennej (lub tablica takich nazw)
$default
zwracana wartość domyślna, jeśli zmienna nie istnieje (jeśli null, to przy braku zmiennej będzie rzucony wyjątek)
$html = false
jesli true to zamiani znaki <, >, " i ' na encje: &lt;, &gt;, &quot;, &#39;,
$sql = false
użyje funkcji ustawionej za pomocą getGPC::$sqlEscapeFun do zabezpieczenia się przed SQL Injection (tak naprawdę tutaj można podstawić dowolną funkcję, ale myślę że takie użycie będzie najpopularniejsze).
$syntax = false
ciąg znaków odpowiadający pierwszemu parametrowi funkcji sprintf. Jako kolejne parametry zostaną użyte podane wszystkie wartośći zmiennych podanych jako parametr $name metody getGPC::get[String|Int|Bool].

Jak wiadomo, przestrzenie nazw w PHP nie istnieją. Czasem ich brakuje, czasem nie robi różnicy… Ale jest dość prosty sposób na emulację tego braku.

Definiujemy sobie klasę o nazwie takiej, jak pożądana przestrzeń nazw, na przykład Path. W niej definiujemy sobie jakieś metody, np. join(), split() etc. Metody te definiujemy jako statyczne (static- nie chcemy musieć inicjalizować obiektu i korzystać z jego instancji, tylko chcemy mieć dostęp do samych metod. Klasę warto zdefiniować jako abstrakcyjną (abstract) - wtedy nie będziemy mieć możliwości utworzyć instancji jej obiektu. Efekt końcowy:

abstract class Path {
    public static function join()
    {
        // tresc funkcji
    }
    public static function split()
    {
        //tresc funkcji
    }
}

A korzystamy z niej:

Path::join('a', 'b');
Path::split('a/b/c');

Voila! :)

Jedynym problemem jest tutaj brak możliwości zaimportowania danej funkcji do ‘zwykłej’ przestrzeni nazw, czyli czegoś co w C++ robi dyrektywa using. Ale myślę, że nie po to chcemy stworzyć własną przestrzeń nazw, żeby z niej później wychodzić :)


Jako, że nie mam możliwości dopisania komentarza do właściwego postu (o, tutaj), dodam go u siebie (czemu nie mam możliwości tam? Nie wiem, po ‘Dodaj komentarz’ pojawia mi się biała strona i tyle…). Trackbacka chyba też tam nie ma.

Ja stosuję troszkę inną sztuczkę odnośnie sytuacji opisanej u Jarosława Mężyka. Tak jak on, przetrzymuję login, hasło i inne dane dotyczące bazy w stałych. Ale samo rozdzielenie konfiguracji jest bardziej automatyczne. Mianowicie, sprawdzam wartość zmiennej $_SERVER['HTTP_HOST']. Jeśli równa jest ona ciągowi znaków ‘localhost’, to stosuje ustawienia lokalne, w przeciwnym wypadku - docelowe. Przykład kodu:

<?php
if ($_SERVER['HTTP_HOST'] == 'localhost') {
  define('DATABASE_LOGIN', 'login');
  define('DATABASE_PASSWD', 'passwd');
  define('DATABASE_HOST', 'localhost');
  define('DATABASE_NAME', 'db');
} else {
  define('DATABASE_LOGIN', 'login_docelowy');
  define('DATABASE_PASSWD', 'passwd_docelowy');
  define('DATABASE_HOST', 'host_docelowy');
  define('DATABASE_NAME', 'db_docelowa');
}
?>