RSS
 

Archive for the ‘Języki programowania’ Category

Domknięcia, funkcje anonimowe przestrzenie nazw i dekoratory

19 lis

Pojęcie dekoratora w języku Python to coś więcej niż wzorzec projektowy. To elegancki sposób na zwiększenie możliwości danej funkcji czy metody doskonale wykorzystujący unikalne cechy języka Python. Mechanizm działania dekoratorów oraz sposób ich użycia doskonale opisał Kent S Johnson (opracowanie to można znaleźć także w języku polskim).

Prostym i użytecznym przykładem dekoratora może być np. funkcja mierząca czas wykonania funkcji udekorowanej.

import time
 
def timeit(method):
    """Mierzy czas wykonania funkcji"""
 
    def check_time(*args, **kw):
        ts = time.time()
        result = method(*args, **kw)
        te = time.time()
        log(method.__name__, args, kw, ts, te)
        return result
 
    def log(name, args, kw, time_start, time_end):
        msg = '%r (%r, %r) %2.10f sec' % (name, args, kw, time_end - time_start)
        print msg
 
    check_time.__name__ = method.__name__
    return check_time

Użycie dekoratora w Pythonie to syntaktyczna perełka.

@timeit
def say_hello(param):
    print param
    time.sleep(0.5)

W PHP osiągnięcie czegoś takiego jest po prostu niemożliwe. Od wersji PHP 5.3 język został wzbogacony o nowe cechy takie jak domknięcia, funkcje anonimowe i przestrzenie nazw. Czy owe mechanizmy umożliwiają implementację czegoś co choćby przypominało pythonowy dekorator?

W PHP nie da się zdefiniować funkcji, a potem przypisać jej inną nazwę.

def foo():
    print 'OK'
 
faa = foo
 
faa() # w wyniku otrzymamy: OK

no chyba że użyjemy funkcji anonimowej (ang. anonymous function)

$foo = function() {echo 'OK';};
$faa = $foo;
$faa(); // w wyniku otrzymamy: OK

Dekorowanie funkcji anonimowych przy użyciu domknięć

Możemy zapomnieć (na razie) o dekorowaniu tradycyjnych funkcji ale udekorowanie funkcji anonimowej staje się potencjalnie możliwe.

$foo = function() {echo 'OK';};
 
$foo = function() use ($foo) {
    echo 'Jest '; return $foo();
};
 
$foo(); // w wyniku otrzymamy: Jest OK

Domknięcia (ang. closures) stoją u podstaw programowania w Pythonie, są też od zawsze obecne i powszechnie używane np. w Javascript-cie.

var Example = function()
{ 
    this.public = function() 
    { 
        return "This is a public method"; 
    }; 
 
    var private = function() 
    { 
        return "This is a private method"; 
    };
};
 
Example.public()  // returns "This is a public method" 
Example.private() // error - doesn't work

Ich implementacja w PHP to kwestia dyskusyjna podobnie jak ich użyteczność. Z uwagi na określone cechy PHP domknięcia w tym języku wydają się nie być odpowiednikiem tego mechanizmu znanym z innych języków programowania. W przeprowadzanym przeze mnie teście przydały się jednak i osobiście dostrzegam jeszcze kilka innych ich praktycznych zastosowań. Wracając jednak do głównego wątku tego wpisu.

W miarę prosty sposób udało się udekorować funkcję „foo”. Domknięcie można zamknąć w funkcji dzięki czemu nadaje się do wielokrotnego użytku.

function timeit($func) {
    return function() use ($func) {
        // pobranie wszystkich argumentów funkcji
        $args = func_get_args();
 
        // utworzenie funkcji pomocniczej
        $microtime_float = function()
        {
            list($usec, $sec) = explode(" ", microtime());
            return ((float)$usec + (float)$sec);
        };
 
        // rozpoczęcie pomiaru czasu
        $time_start = $microtime_float();
 
        // wywolanie funkcji
        $result = call_user_func_array($func, $args);
 
        // zakończenie pomiaru czasu i wyświetlenie wyników
        $time_end = $microtime_float();
        $time = $time_end - $time_start;
        printf ("\nfunction was executed during %01.6f seconds\n", $time);
 
        // zwrócenie resultatu działania funkcji
        return $result;
    };
}

Zaprezentuje może użycie „dekoratora” timeit na funkcji wymagającej parametrów.

$func_say_hello = function(param) {
	return 'Hello '. $param;
};
 
$func_say_hello = timeit($func_say_hello);
echo $func_say_hello('World');

Funkcje anonimowe są, a raczej będą stosunkowo rzadko używane, podobnie zresztą jak funkcje lambda w Pythonie, których są odpowiednikiem. Pomiar czasu wykonania np. funkcji sortującej to z pewnością przydatna sprawa, ale wzbogacanie funkcji anonimowych o nowe możliwości przy pomocy „dekoratorów” wydaje się być mało przydatne. Czy zatem można udekorować zwykłą funkcję?

Czy przestrzenie nazw pomagają w dekorowaniu funkcji?

Po co nam przestrzenie nazw (ang. namespaces)? Załóżmy, że mamy w pliku functions.php zdefiniowaną przestrzeń nazw „lib” i funkcję „say_hello”

<?php
namespace lib {
    function say_hello($param)
    {
        return 'Hello '. $param;
    }
}

Gdybyśmy w pliku index.php dołączyli plik functions.php moglibyśmy używać zdefiniowanych w nim funkcji.

<?php
include './functions.php';
echo \lib\say_hello('World');

Wywołanie say_hello(‘World’); bez podania poprzedzającego go namespace-a zakończyłoby się błędem. Niestety PHP nie wspiera jak na razie możliwości aliasowania funkcji czy stałych zamkniętych w przestrzeni nazw dlatego też chęć odwoływania się do funkcji „say_hello” bezpośrednio musi zostać poprzedzona definicją nazwijmy to „atrapy”, która przy okazji pozwoli udekorować opakowywowaną funkcję.

include './functions.php';
 
function timeit($func) {...}
 
function say_hello($param)
{
    $func_say_hello = '\lib\say_hello';
    $func_say_hello = timeit($func_say_hello);
    $result = $func_say_hello($param);
}
echo say_hello('World');

Zwróćmy uwagę na to, że funkcje „say_hello” i „\lib\say_hello” to nie to samo i równie dobrze moglibyśmy nie używać przestrzeni nazw i osiągnęlibyśmy to samo (tylko nazwy funkcji byłyby inne)

function timeit($func) {...}
 
function say_hello($param) {...}
 
function decorated_say_hello($param)
{
    $func_say_hello = 'say_hello';
    $func_say_hello = timeit($func_say_hello);
    $result = $func_say_hello($param);
}

Z punktu widzenia wygody użycia to czy użyjemy przestrzeni nazw czy nie to mamy to samo. I tak chcąc zmierzyć czas wykonania funkcji „say_hello” musimy zmienić jej wywołanie w kodzie na „decorated_say_hello”. Nie da się udekorować zdefiniowanych funkcji w pythoniczny sposób.

Aliasy – nowe możliwości dekorowania klas

W przypadku funkcji moje rozważania to jedynie gimnastyka mózgu, jednak w przypadku klas teoretyczne dywagacje – jak się zaraz okaże – przybiorą praktyczną formę.

Wspominałem wyżej o aliasach. Tego czego nie da się zrobić z funkcjami i stałymi w PHP > 5.3 da się uczynić z klasami.

Załóżmy, że plik functions.php wygląda tak

namespace lib {
	class Say
	{
		public static function hello($param)
		{
			return 'Hello '. $param;
		}
 
		public function goodbye($param)
		{
			return 'Goodbye '.$param;
		}
	}
}

Do pliku index.php możemy dołączyć functions.php i przypisać klasie „/lib/Say” alias Say

include './functions.php';
 
function timeit($func) {...}
 
use \lib\Say as Say;
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Nadanie aliasu sprawia, że możemy używać klasy tak jakby była zdefiniowana bez użycia przestrzeni nazw. Jeśli w tym momencie przyjdzie nam ochota zmierzenia czasu wykonania metod klasy „Say” (a właściwie „\lib\Say”) bez zmiany wywołań musimy zrobić dwie rzeczy. Zakomentować linijkę nadającą alias oraz udekorować klasę.

include './functions.php';
 
function timeit($func) {...}
 
//use \lib\Say as Say;
 
class Say
{
	private $_obj;
 
	public function  __construct() {
		$this->_obj = new \lib\Say();
	}
 
	public static function hello($param)
	{
		$func = '\lib\Say::hello';
		$func = \deco\timeit($func);
		return $func($param);
	}
 
	public function goodbye($param)
	{
		$func = function($obj, $param) {
			return $obj->goodbye($param);
		};
 
		$func = \deco\timeit($func);
		return $func($this->_obj, $param);
	}
}
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Wywołanie metod się nie zmieniło, ale wzbogaciliśmy naszą klasę o funkcjonalność pozwalającą nam zmierzyć czas wykonania zarówno metody statycznej klasy, jak i metody instancji. Do wygody użycia dekoratora „@timeit” w Pythonie jest jeszcze bardzo daleko. Udekorowaliśmy zaledwie dwie metody, a trzeba było napisać tyle kodu. Z każdą kolejną metodą przybędzie dodatkowego kodu w dekoratorze. Czy da się ten przyrost ograniczyć?. Dzięki metodom magicznym da się zrobić bardziej uniwersalny dekorator.

include './functions.php';
 
function timeit($func) {...}
 
//use \lib\Say as Say;
 
class Say
{
	private $_obj;
 
	public function  __construct() {
		$this->_obj = new \lib\Say();
	}
 
	public static function  __callStatic($name, $arguments)
	{
		$func = '\lib\Say::'.$name;
		return self::_addDecorators($func, $arguments);
	}
 
	public function __call($name, $arguments)
	{
		$func = function() {
			$params = func_get_args();
			$obj = array_shift($params);
			$method = array_shift($params);
			return call_user_func_array(array($obj, $method), $params);
		};
 
		array_unshift($arguments, $name);
		array_unshift($arguments, $this->_obj);
		return self::_addDecorators($func, $arguments);
	}
 
	private static function _addDecorators($func, $arguments)
	{
		$func = \deco\timeit($func);
		return call_user_func_array($func, $arguments);
	}
}
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Konkluzje

  1. To naprawdę działa – doznałem intelektualnego orgazmu.
  2. Nie stosujcie takich rozwiązań w prawdziwym projekcie

Próba debugowania takiego kodu skazana jest z góry na porażkę. Przeprowadzony przeze mnie eksperyment dostarczył mi najlepszego dowodu na to, że nie ma sensu implementować na siłę mechanizmów znanych z innych języków. PHP ma swoją naturę, która czasami jest ograniczeniem, ale często stanowi też jego zaletę.

 
No Comments

Posted in PHP, Python

 

Pogodzenie Python-a z PHP na Apache2 (mod_wsgi + mod_php)

09 paź

Wstęp

Poradników i tutoriali opisujących konfigurację Apache z PHP i MySQL pod Linuxem (LAMP) jest cała masa. W Ubuntu sprowadza się to do wywołania w konsoli prostego polecenia

sudo apt-get install php5 php5-mysql mysql-server apache2 libapache2-mod-auth-mysql

… i mamy wszystko co potrzeba. Więcej na ten temat znajdziemy choćby w artykule Instalacja Apache + PHP5 + MySQL.

Instalacja frameworka Django i preferowanego przez pythonowców serwera baz danych Postgres jest równie prosta – przynajmniej pod Ubuntu.

sudo apt-get install python-django postgresql python-psycopg2 postfix python-imaging python-docutils

Django ma jedną z najlepszych dokumentacji, a o Postgresie pisze niemal co drugi bloger, który miał coś z nim do czynienia tak więc zainteresowanych po prostu odeślę do wciąż aktualnego i w miarę kompletnego wpisu Instalacja PostgreSQL 8.3 w Ubuntu 8.04.

O ile hostowanie skryptów PHP na Apache-u jest rzeczą naturalną zarówno w środowisku deweloperskim jak i produkcyjnym o tyle w przypadku Pythona a konkretnie aplikacji Django już nie. Django posiada świetny wbudowany serwer stworzony na podstawie klasy BaseHttpServer ze standardowej biblioteki Pythona, który świetnie sprawdza się do szybkiego testowania aplikacji o niepełnej funkcjonalności. Restartuje się automatycznie przy każdej zmianie w kodzie a ponadto ponieważ uruchamiany jest w terminalu wyświetla na wyjściu wszystkie instrukcje print.

Użycie serwera wbudowanego w środowisku produkcyjnym jest zabronione licencją, a poza tym istnieje jeszcze kilka innych obiektywnych powodów aby tego nie robić, jak choćby bezpieczeństwo i stabilność, czy wydajność. Jak się okazuje także dla celów developerskich warto czasem podjąć trud konfiguracji serwera zewnętrznego.

W przypadku Apache-a w kontekście hostowania skryptów napisanych w języku Python najczęściej stosuje się dwa rozwiązania tj. mod_python lub mod_wsgi. Oba z modułów nie powodują konfliktów z mod_php, tak więc chcąc na swoim serwerze obsługiwać zarówno aplikacje PHP i Python można zastosować dowolne z wymienionych rozwiązań.

Ponoć konfiguracja Apache2 + mod_python jest częściej stosowana, lepiej przetestowana i udokumentowana, ja jednak zdecydowałem się na WSGI z uwagi na elastyczność jaką udało mi się uzyskać.

Założenia

W przypadku środowisk produkcyjnych kluczową kwestią jest bezpieczeństwo i stabilność serwera. W środowisku developerskim, w którym skrypty uruchamiane są jedynie na komputerze lokalnym osobiście stawiam na elastyczność kosztem nawet bezpieczeństwa i stabilności. Nie lubię grzebać w plikach konfiguracyjnych Apache-a i edytowanie pliku /etc/apach2/httpd.conf lub któregoś z plików w katalogu /etc/apache2/sites-available/ za każdym razem kiedy tworze nowy projekt lub zmieniam jego nazwę. W przypadku projektów realizowanych w języku PHP wystarczy, że w moim katalogu workspace stworzę nowy katalog np. „nowy_projekt_php”, wrzucę do niego plik index.php i wywołam w pasku adresu przeglądarki

http://localhost/nowy_projekt_php
Podobny efekt chciałem uzyskać w przypadku projektów realizowanych w Django.

Instalacja mod_wsgi

Moduł WSGI możesz zainstalować ze źródeł, co zostało przystępnie opisane w artykule Ubuntu Hardy – mod_wsgi Installation, ale w Ubuntu można skorzystać z repozytorium:

sudo apt-get install libapache2-mod-wsgi

Aby aktywować moduł (w Ubuntu 10.04 – można pominąć)

sudo a2enmod mod-wsgi

W przypadku instalacji z repozytorium wystarczy już tylko restart serwera.

sudo /etc/init.d/apache2 restart

Konfiguracja Apache

Integracja mod_wsgi z Django jest świetnie opisana zarówno na stronach dokumentacji Django, jak też (może nawet lepiej) na stronie rozszerzenia specjalnie poświęconej temu zagadnieniu w artykule pod pt. Integration With Django.

Ja jednak zaproponuję jeszcze inne rozwiązanie na które natknąłem się na forum w wątku Ubuntu + Apache2 + WSGI (mod_wsgi) with LAMP i lekko zmodyfikowałem na swoje potrzeby.

Aby osiągnąć wyżej opisane założenie w najprostszy sposób należy:

Stworzyć Virtualhosta w pliku httpd.conf

sudo vim /etc/apache2/httpd.conf

<VirtualHost *>
    ServerName testhost
    DocumentRoot /home/myuser/workspace/

    <Directory /home/myuser/workspace/>
        Options Indexes FollowSymLinks MultiViews ExecCGI

        AddType application/x-httpd-php .php .phtml
        AddType application/x-httpd-php-source .phps
        AddHandler application/x-httpd-php .php .phtml
        AddHandler cgi-script .cgi
        AddHandler wsgi-script .wsgi

        Order allow,deny
        allow from all
    </Directory>
</Virtualhost>

W przeciwieństwie do rozwiązania zaproponowanego na forum proponuję usunąć dyrektywę „AllowOverride None” dzięki czemu możliwe będzie skorzystanie z mod_rewrite.

Trzeba jeszcze poinformować serwer, że domyślnym plikiem w aplikacji napisanej w Pythonie będzie index.wsgi

sudo gedit /etc/apache2/mods-enabled/dir.conf

<IfModule mod_dir.c>
    DirectoryIndex index.html index.cgi index.pl index.php index.xhtml index.htm index.wsgi
</IfModule>

W katalogu /home/myuser/workspace/ utwórzmy nowy podkatalog nowy_projekt_python a w nim plik index.wsgi o treści:

def application(environ, start_response):
    status = '200 OK' 
    output = 'Hello World!'
 
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(output)))]
    start_response(status, response_headers)
 
    return [output]

Jeśli po wywołaniu


http://localhost/nowy_projekt_python

widać „Hello World!” to znaczy, że wszystko jest ok, jeśli nie to może wystarczy zrestartować serwer ;)

Twórcy Django dążą do tego aby ich framework był pythonowy jak to tylko możliwe dlatego np. konfigurację trzymają w plikach py a nie np. w xml albo yml-u. Dzięki takiemu podejściu możemy mieć pewność, że po wrzuceniu do katalogu nowy_projekt_python aplikacji Django i modyfikacji pliku index.wsgi, zobaczymy w przeglądarce to co byśmy widzieli po uruchomieniu serwera wbudowanego.

Zmodyfikujmy index.wsgi

import os, sys
sys.path.append('/home/myuser/workspace')
sys.path.append('/home/myuser/workspace/nowy_projekt_python')
 
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
 
import django.core.handlers.wsgi
 
_application = django.core.handlers.wsgi.WSGIHandler()
 
def application(environ, start_response):
	if environ['wsgi.url_scheme'] == 'https':
		environ['HTTPS'] = 'on'
	return _application(environ, start_response)

Przydałby się jeszcze plik .htaccess

<IfModule mod_rewrite.c>
    # Turn on URL rewriting
    RewriteEngine On

    # Installation directory
    RewriteBase /nowy_projekt_python/

    # Protect hidden files from being viewed
    <Files .*>
        Order Deny,Allow
        Deny From All
    </Files>

    # 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.wsgi/URL
    RewriteRule .* index.wsgi/$0 [PT]

</IfModule>

Zaproponowane tutaj rozwiązanie jest jedynie punktem wyjścia do stworzenia swojej własnej uniwersalnej konfiguracji środowiska developerskiego do pracy z projektami PHP i Python. Zapewne przydałoby się parę rzeczy inaczej skonfigurować lub też dodać kilka opcji jak np. dyrektywę zapobiegającą wyświetlaniu treści plików python w przeglądarce. Zachęcam do eksperymentowania i dzielenia się swoim doświadczeniem.

 
2 Comments

Posted in PHP, Python

 

Obsługa __call w kontrolerach Kohana 3

03 paź

Obsługa magicznej metody __call w kontrolerach Kohana 3 została zarzucona. W zamian za to Twórcy frameworka zaimplementowali obsługę tzw „catch all-a”. Definicje ścieżek routingu definiuje się w pliku application/bootstrap.php

U mnie „catch all” wygląda mniej więcej tak.

Route::set('catch_all', '<path>', array('path' => '.+'))
    ->defaults(array(
        'controller' => 'error',
        'action' => '404',
));

Czyli jak łatwo się domyślić w przypadku nie istnienia danego kontrolera i akcji zwracana jest strona 404.

Jednak zamiast kontrolera error można by w tym miejscu wskazać dowolny inny kontroler a w miejsce 404 dowolną inną metodę.

Route::set('catch_all', '<path>', array('path' => '.+'))
    ->defaults(array(
        'controller' => 'page',
        'action' => 'index',
));

Dzięki takiemu podejściu z url-a można wyeliminować zarówno nazwę kontrolera jak i metody i ograniczyć się jedynie do unikalnego identyfikatora zasobu.

Wadą tego rozwiązania jest to, że wariant ten jest rozpatrywany dopiero na samym końcu, a poza tym wywołanie adresu na kształt http://domena.com/page/nie_istniejaca_akcja spowoduje rzucenie wyjątku

exception 'ReflectionException' with message 'Method action_nie_istniejaca_akcja does not exist'...

Wyjątek ten można oczywiście obsłużyć w bootstrapie tym bardziej, że parametr ‘status’ instancji klasy Request jest ustawiany na 404.

$request = Request::instance();
try {
    $request->execute();
    echo $request
        ->send_headers()
        ->response;
} catch (Exception $e) {
    if ($request->status == '404') {
        Request::factory('error/404')->execute();
    }
}

Co jednak kiedy chcemy mieć możliwość ograniczenia mechanizmu „catch all” do konkretnego kontrolera albo użyć go w więcej niż jednym kontrolerze? Jedynym rozwiązaniem wydaje mi się modyfikacja klasy Request. Nie namawiam oczywiście do grzebania w katalogu „system”. Kaskadowy układ plików Kohany pozwala na utworzenie pliku request.php w katalogu application/classes i zdefiniowanie klasy „Request” dziedziczącej po „Kohana_Request”. Modyfikacji wymaga w zasadzie jedna linia w metodzie „execute”.

Zamiast

$class->getMethod('action_'.$action)->invokeArgs($controller, $this->_params);

należy najpierw sprawdzić czy dana metoda istnieje, a podjąć próbę wywołania magicznej metody __call.

if($class->hasMethod('action_'.$action) === true)
{
    $class->getMethod('action_'.$action)->invokeArgs($controller, $this->_params);
}
else
{
    $class->getMethod('__call')->invokeArgs($controller, array('action_'.$action, $this->_params));
}
 
No Comments

Posted in PHP

 

PHP 5.3 – wywołanie array_multisort za pośrednictwem call_user_func_array

08 cze

Chcąc dostosować swój kod do wersji PHP 5.3, tak aby wykonywał się bezproblemowo z uwzględnieniem błędów STRICT natrafiłem na taki drobny szczegół. Swego czasu dodałem do manuala funkcji array_multisort na php.net taką drobną [notkę zawierającą implementację funkcji ułatwiającej sortowanie wyników zapytań sql-owych] (http://usphp.com/manual/en/function.array-multisort.php#87268).

Chcąc ją dzisiaj użyć dostałem na twarz warning-a „Parameter 2 to array_multisort() expected to be a reference, value given”.

Załóżmy, że miałem następujące dane.

$data = array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
);

Aby posortować tę tablicę według kolumny „name” wywołałem wspomnianą funkcję w następujący sposób

sortDbResult($data, 'name', SORT_ASC, SORT_STRING);

Przyjrzałem się problematycznej linii i zobaczyłem, że wywołuję funkcję array_multisort przy pomocy funkcji call_user_func_array, do której parametry przekazuję za pośrednictwem tablicy.

call_user_func_array('array_multisort', $_params);

W moim przypadku zmienna $_params miała następującą zawartość

array(
0 => &array (
	0 => 'Rodzaje',
	1 => 'Kategorie',
	2 => 'Statusy',
	3 => 'Kolory',
),
1 => 2,
2 => 4,
3 => array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
));

Pomyślałem sobie o co chodzi z tym błędem w końcu drugi parametr funkcji array_multisort to w moim przypadku wartość stałej SORT_ASC czyli 2. Przeprowadziłem mały eksperyment i wywołałem funkcję array_multisort bezpośrednio.

$data_1 = array (
	0 => 'Rodzaje',
	1 => 'Kategorie',
	2 => 'Statusy',
	3 => 'Kolory',
);
 
$data_2 = array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
);
array_multisort($data_1, SORT_ASC, SORT_STRING, $data_2);
 
var_dump($data_2);

Zadziałało. Tablica $_data2 została posortowana prawidłowo.

call_user_func_array('array_multisort',  array(&$data_1, SORT_ASC, SORT_STRING, &$data_2));

Powyższy kod również wykonał się nie zwracając żadnych komunikatów błędów.

$params = array(&$data_1, SORT_ASC, SORT_STRING, &$data_2);
call_user_func_array('array_multisort',  $params);

Wykonanie powyższego kodu kończy się już niestety warningiem. Aby się go pozbyć trzeba użyć małego obejścia i przypisać wartości stałych do zmiennych a następnie przekazać je do tablicy przy pomocy referencji.

$o = SORT_ASC;
$v = SORT_STRING;
$params = array(&$data_1, &$o, &$v, &$data_2);
call_user_func_array('array_multisort', $params);

W tym wypadku nie działa ukrywanie błędów. Funkcja array_multisort po prostu nie zadziała prawidłowo.

Poprawiona funkcja sortDbResult wygląda następująco:

/**
 * Sort DB result
 *
 * @param array $data Result of sql query as associative array
 *
 * Rest of parameters are optional
 * [, string $name  [, mixed $name or $order  [, mixed $name or $mode]]]
 * $name string - column name i database table
 * $order integer - sorting direction ascending (SORT_ASC) or descending (SORT_DESC)
 * $mode integer - sorting mode (SORT_REGULAR, SORT_STRING, SORT_NUMERIC)
 *
 * 
 *  $i,
 *                      'first_name' => sprintf('first_name_%s', rand(1, 9)),
 *                      'last_name' => sprintf('last_name_%s', rand(1, 9)),
 *                      'date' => date('Y-m-d', rand(0, time()))
 *                  );
 * }
 * $data = sortDbResult($data, 'date', SORT_DESC, SORT_NUMERIC, 'id');
 * var_dump($data);
 * $data = sortDbResult($data, 'last_name', SORT_ASC, SORT_STRING, 'first_name', SORT_ASC, SORT_STRING);    
 * var_dump($data);
 * ?>
 * 
 *
 * @return array $data - Sorted data
 */

function sortDbResult(array $data /*$name, $order, $mode*/) {
	$_argList = func_get_args();
	$_data = array_shift($_argList);
	if (empty($_data)) {
		return $_data;
	}
	$_max = count($_argList);
	$_params = array();
	$_cols = array();
	$_rules = array();
	for ($_i = 0; $_i < $_max; $_i += 3)
	{
		$_name = (string) $_argList[$_i];
		if (!in_array($_name, array_keys(current($_data)))) {
			continue;
		}
		if (!isset($_argList[($_i + 1)]) || is_string($_argList[($_i + 1)])) {
			$_order = SORT_ASC;
			$_mode = SORT_REGULAR;
			$_i -= 2;
		} else if (3 > $_argList[($_i + 1)]) {
			$_order = SORT_ASC;
			$_mode = $_argList[($_i + 1)];
			$_i--;
		} else {
			$_order = $_argList[($_i + 1)] == SORT_ASC ? SORT_ASC : SORT_DESC;
			if (!isset($_argList[($_i + 2)]) || is_string($_argList[($_i + 2)])) {
				$_mode = SORT_REGULAR;
				$_i--;
			} else {
				$_mode = $_argList[($_i + 2)];
			}
		}
		$_mode = $_mode != SORT_NUMERIC
					? $_argList[($_i + 2)] != SORT_STRING ? SORT_REGULAR : SORT_STRING
					: SORT_NUMERIC;
		// references below required from PHP 5.3
		$_rules[] = array('name' => $_name, 'order' => &$_order, 'mode' => &$_mode);
	}
	foreach ($_data as $_k => $_row) {
		foreach ($_rules as $_rule) {
			if (!isset($_cols[$_rule['name']])) {
				$_cols[$_rule['name']] = array();
				$_params[] = &$_cols[$_rule['name']];
				// references below required from PHP 5.3
				$_params[] = &$_rule['order'];
				$_params[] = &$_rule['mode'];
			}
			$_cols[$_rule['name']][$_k] = $_row[$_rule['name']];
		}
	}
	$_params[] = &$_data;
	call_user_func_array('array_multisort', $_params);
	return $_data;
}
 
No Comments

Posted in PHP

 

Jak wydobyć rozszerzenie z nazwy pliku.

06 kwi

Jednym z ważniejszych zabiegów optymalizujących kod aplikacji jest używanie przy wykonywaniu poszczególnych operacji, funkcji do tego dedykowanych. Powszechną jest opinia o nadużywaniu np. wyrażeń regularnych, czy też pisaniu własnych funkcji, których odpowiedniki są już zaimplementowane w samym PHP.

Wydobycie rozszerzenia z nazwy pliku nie jest operacją zbyt skomplikowaną i zasobożerną jednak da się ją wykonać na kilka różnych sposobów. Postanowiłem przeprowadzić mały test sprawdzający, które z przyjętych rozwiązań wykona owo zadanie najsprawniej.

Na początek przedstawię kilka sposobów wyekstrahowania rozszerzenia z pełnej ścieżki do pliku.

Na początek użycie funkcji pathinfo

function get_ext_using_pathinfo($path) {
	$parts = pathinfo($path);
	return $parts['extension'];
}

Użycie funkcji pathinfo ze stałą PATHINFO_EXTENSION – dostępną od wersji PHP 5.2

function get_ext_using_pathinfo_with_const($path) {
	return pathinfo($path, PATHINFO_EXTENSION);
}

Użycie funkcji operujących na tekście substr i strrpos

function get_ext_using_substr_and_strrpos($path) {
	return substr($path, strrpos($path, ".") + 1);
}

Użycie wyrażeń regularnych a dokładnie funkcji preg_replace

function get_ext_using_preg_replace($path) {
	return preg_replace('/.*\.([a-zA-Z0-9]*)$/', '\\1', $path);
}

Użycie wyrażeń regularnych z tymże tym razem funkcji preg_match

function get_ext_using_preg_match($path) {
	preg_match('/.*\.([a-zA-Z0-9]*)$/', $path, $matches);
	return $matches[1];
}

Wykonanie exploda i wydobycie ostatniego elementu tablicy funkcją array_pop

function get_ext_using_explode($path) {
	return array_pop(explode('.', $path));
}

Przez każdą z wyżej wymienionych funkcji – w pętli for – 10 tys. razy przepuściłem zmienną zawierającą przykładową ścieżkę do pliku.

$path = '/home/user/workplace/project/images/file.sufix.ext';

Operację tę wykonałem kilkukrotnie zapisując wyniki do pliku. Po uśrednieniu i zaokrągleniu wyników wyszło mi:

substr i strrpos preg_replace preg_match pathinfo ze stałą PATHINFO_EXTENSION explode i array_pop pathinfo
0.077 0.0822 0.0904 0.0952 0.1049 0.1310

Jak wynika z powyższego zestawienia najlepiej spisały się funkcje operujące na stringach. Funkcje preg_replace i preg_match pozytywnie mnie zaskoczyły gdyż spodziewałem się, że zaprzęgnięcie do tak prostej pracy tak potężnej maszyny jaką są wyrażenia regularne będzie obarczone sporą stratą czasu. Z kolei bardzo zawiodłem się na wbudowanej funkcji pathinfo, którą dotąd najczęściej wykorzystywałem do wydobycia potrzebnych mi informacji ze ścieżki. Myślałem, że jest bardziej wydajna, a tu się okazało, że w wersji uniemożliwiającej użycie stałej PATHINFO_EXTENSION jest ona wolniejsza nawet od exploda. Nie skreślałbym tej funkcji w przypadku chęci uzyskania kompletu danych o ścieżce jednak dla atomowej operacji wydobycia rozszerzenia warto pomyśleć o innym sposobie. Z drugiej strony, różnice jak widać nie są znowu tak wielkie, więc dla tego prostego zadania w 99% przypadków użycia nie będzie miało znaczenia, którego z rozwiązań użyjemy i spokojnie możemy kierować się wygodą.

 
 

Tworzenie obiektów w JavaScript

21 mar

W JavaSripcie istnieje bazowa klasa Object, która stanowi prototyp wszystkich pozostałych obiektów JavaScript. Można jej użyć bezpośrednio do utworzenia obiektu.

    obj = new Object();
    obj.x = 1;
    obj.y = 2;

lub alternatywnie

    obj = {};
    obj.x = 1;
    obj.y = 2;

Nawiasy klamrowe stanowią semantyczny skrót podobnie jak użycie nawiasów kwadratowych (arr = []) może zastąpić arr = new Array(). Tablica czyli Array jest jednym z wielu wbudowanych w JavaScript obiektów podobnie jak Window, Document itd.

Aby utworzyć niestandardowy obiekt należy najpierw zdefinować prototyp Klasy, której instancją dany obiekt będzie. Najprościej utworzyć prototyp metodą konstruktora.

var Foo = function() {
    this.x = 1;
    this.y = 2;
    this.sum = function() {
       return this.x + this.y;
    }
}

W konstruktorze definiujemy właściwości i metody. Nową instancję tworzy się z użyciem operatora new.

    obj = new Foo();
    alert(obj.x);
    alert(obj.sum());

Do właściwości lub metod można się odwołać za pośrednictwem utworzonego w ten sposób obiektu, ale można też pominąć operację przypisania do zmiennej i wywołać metodę w sposób niemożliwy np. w PHP.

	alert(new Foo().sum());

Wywołując obj = new Foo(); powołuje się do życia instancje prototypu Foo. Można następnie wywołać metodę obj.sum();. Metodę sum można też wywołać z pominięciem przypisania obiektu do zmiennej new Foo().someMethod(); Tak czy inaczej zawsze w trzeba w pierwszej kolejności utworzyć obiekt.

Definiowanie klas metodą prototypu jest łatwe i intuicyjne, nie mniej w przypadku powoływania wielu obiektów danego prototypu zalecany jest sposób definiowania metod z użyciem prototype (nie mylić z frameworkiem prototype).

var Foo = function() { 
    this.x = 1;
    this.y = 2;
};
 
Foo.prototype.sum = function() {
   return this.x + this.y;
}

Różnica polega na tym, że w pierwszym przypadku przy powoływaniu do życia nowej instancji za każdym razem metoda „sum” tworzona jest na nowo, a w drugim przypadku jest to cały czas jedna i ta sama metoda. Zyskuję się na zaoszczędzonej pamięci co przy bardzo rozbudowanych skryptach nabiera znaczenia.

Zdefiniowanie metody z pominięciem słowa „prototype”:

var Foo = function() { };
 
Foo.sum = function(x, y) {
    return x+ y;
}

… daje możliwość wywołania metody klasy bez tworzenia obiektu Foo.sum(2,4); – to taki jakby odpowiednik metod statycznych w PHP. Analogicznie do metod statycznych w PHP użycie „this” wewnątrz takiej funkcji mija się z celem, więc gdybyśmy przez przypadek zdefiniowali:

var Foo = function() { 
    this.x = 1;
    this.y = 2;
};
 
Foo.sum = function() {
   return this.x + this.y;
}

to wywołanie Foo.sum(); zwróci „NaN” a z kolei new Foo().sum(); zakończy się błędem „TypeError on line 1: (new Foo).sum is not a function”.

W JS można też utworzyć obiekt ad hock

var Foo = { x: 1, y: 2, sum: function () { return this.x + this.y; } } W tym wypadku Foo nie jest prototypem tylko instancją prototypu Object dlatego oczywistym jest wywołanie Foo.sum(); z pominięciem operatora new.

 
 

Pobieranie listy plików o wybranym rozszerzeniu w PHP

22 lut

Aby pobrać listę plików o wybranym rozszerzeniu z danego katalogu najprościej jest użyć funkcji glob

glob("*.jpg");

Jeśli chcemy pobrać pliki z danego katalogu i jego podkatalogów trzeba sobie dopisać wersję rekurencyjną funkcji glob:

function glob_recur($pattern='*', $flags = 0, $path = '')
{
    $paths = glob($path.'*', GLOB_MARK|GLOB_ONLYDIR|GLOB_NOSORT);
    $files = glob($path.$pattern, $flags);
    foreach ($paths as $path) {
        $files = array_merge($files, glob_recur($pattern, $flags, $path));
    }
    return $files;
}

Funkcja glob umożliwia użycie tzw. wilkardów czyli w najprostszym ujęciu pytanik „?” zastępuje dowolny znak, a gwiazdka „*” dowolny ciąg znaków. Wilkardy to nie wyrażenia regularne i szybko odczujemy różnicę choćby w przypadku, kiedy chcielibyśmy pobrać pliki o rozszerzeniu np. css i js. W takiej sytuacji musielibyśmy dwa razy wywołać funkcję glob. Do dyspozycji mamy jednak funkcję scandir, która ułatwia listowanie katalogu.

function get_files_from_dir($path, $extensions = array())
{
    $entries = scandir($path.'/');
    $files = array();
    foreach($entries as $entry)
    {
        if ($entry == '.' || $entry == '..') continue;
 
        $_path = $path.'/'.$entry;
        if(is_file($_path)) {
            if (!empty($extensions))
            {
                $ext = pathinfo($filename, PATHINFO_EXTENSION);
                if (!in_array($ext, $extensions)) continue;
            }
            $files[] = $_path;
        }
    }
    return $files;
}

Scandir wybierze wszystkie pliki i katalogi z zadanej lokalizacji. Odróżnienie plików od katalogów to prosty tekst przy użyciu funkcji is_file lub is_dir z kolei wydobycie rozszerzenia z pliku można przeprowadzić na kilka sposobów:

$ext = pathinfo($filename, PATHINFO_EXTENSION);
// lub
$ext = preg_replace('/.*\.([\d\w]{2,5})/i', '$1', $filename); //zakładam, że rozszerzenie ma od 2 do 5 znaków
// lub 
$ext = substr($filename , strrpos($filename , '.') +1);

Modyfikacja funkcji get_files_from_dir tak aby działała rekurencyjnie wymaga jedynie niewielkiej zmiany:

function get_files_from_dir_recur($path, $extensions = array())
{
    $entries = scandir($path.'/');
    $files = array();
    foreach($entries as $entry)
    {
        if ($entry == '.' || $entry == '..') continue;
 
        $_path = $path.'/'.$entry;
        if(is_file($_path)) {
            if (!empty($extensions))
            {
                $ext = pathinfo($filename, PATHINFO_EXTENSION);
                if (!in_array($ext, $extensions)) continue;
            }
            $files[] = $_path;
        } else if (is_dir($_path)) {
            $files = array_merge($files, self::get_files_from_dir_recur($path, $extensions));
        }
    }
    return $files;
}

W zasadzie to samo można osiągnąć za pomocą iteratorów z biblioteki SPL:

function get_files_from_dir($path, $extensions = array())
{
    $files = array();
    $it = new DirectoryIterator($path);
    foreach($it as $file) {
        $filename = $file->getFilename();
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
        if ($file->isFile() && (empty($extensions) || in_array($ext, $extensions))) {
            $files[] = $file->getPathname();
        }
    }
    return $files;
}

i wersja rekurencyjna

function get_files_from_dir_recur($path, $extensions = array())
{
    $files = array();
    $it = new RecursiveDirectoryIterator($path);
    foreach(new RecursiveIteratorIterator($it) as $file) {
        $filename = $file->getFilename();
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
        if ($file->isFile() && (empty($extensions) || in_array($ext, $extensions))) {
            $files[] = $file->getPathname();
        }
    }
    return $files;
}

Funkcja glob jest jest najprostsza w użyciu. Funkcja z użyciem scandir sprawdza się kiedy glob zawodzi i jest szybsza od iteratorów, które dla każdego znalezionego pliku powołują do życia obiekt.

Kiedy sięgać po iteratory i czy w ogóle? W podanych przeze mnie przykładach interesują nas jedynie ścieżki do plików, tymczasem iteratory zwracają pliki w formie obiektów SplFileInfo z których można wyciągnąć dużo więcej informacji o plikach. Niekiedy może się to przydać i wtedy użycie iteratorów jest zasadne. Jeśli jednak chcemy otrzymać jedynie listę plików wtedy scandir jest lepszym rozwiązaniem bo szybszym.

Funkcja scandir i iteratory są dostępne dopiero od 5 wersji PHP. W PHP4 zawartość katalogu możymy odczytać dzięki funkcji opendir.

function get_files_from_dir($path, $extensions = array()) {
    $files = array();
    if (!is_dir($path)) {
        return $files;
    }
    if ($dh = opendir($path)) {
        while (($file = readdir($dh)) !== false) {
            $ext = pathinfo($file, PATHINFO_EXTENSION);
            if (empty($extensions) || in_array($ext, $extensions)) {
                $files[] = $path.'/'.$file;
            }
        }
        closedir($dh);
    }
    return $files;
}
 
1 Comment

Posted in PHP

 

is_ajax, is_cli, is_ssl

16 sty

Zarejestrowałem się w serwisie 9fingers stworzonym przez helion jako portal społecznościowy dedykowany programistom. Spotkałem się tam z pytaniem o Najlepszy projekt Open Source do nauki PHP. Autorowi mówiąc krótko chodziło o to na jakim projekcie się wzorować? Cóż osobiście nie znam najlepszego projektu, ale za to wiele równie dobrych, po analizie których można się wielu rzeczy dowiedzieć. Wszystko zależy od tego jaki poziom się reprezentuje i czego człowiek szuka. Zasad ogólnych, tzw. dobrych praktyk uczył bym się raczej z książek, natomiast nic nie stoi na przeszkodzie aby podejrzeć sposób zaimplementowania konkretnych rozwiązań. Powtarzam podejrzeć i dodaję zrozumieć. Nie mam tu na myśli od razu całych frameworków.

Ja na przykład podpatrzyłem kilka prostych, ale bardzo przydatnych funkcji w php wykrywających w jakim trybie pracuje nasza aplikacja.

Metoda jednego z helperów w Kohana PHP Framework sprawdzająca czy żądanie do hosta przyszło za pośrednictwem tzw. AJAX-a. Bardzo przydatna kiedy człowiek nie chce pisać osobnych kontrolerów do obsługi zapytań przychodzących w sposób tradycyjny i via AJAX.

/**
 * Tests if the current request is an AJAX request by checking the X-Requested-With HTTP
 * request header that most popular JS frameworks now set for AJAX calls.
 *
 * @return  boolean
 */
public static function is_ajax()
{
	return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) AND strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
}
// From Kohana PHP Framework

Jeśli pracujesz nad aplikacją w której w grę wchodzą rozliczenia finansowe albo dane poufne to przyda Ci się funkcja rodem z Word Pressa sprawdzająca czy użytkownik łączy się z serwerem używając SSL-a. W przypadku wykrycia uchybienia wystarczy przekierowanie na adres z protokołem HTTPS.

/**
 * Check if accessed via HTTPS
 *
 * Apache leaves ,$_SERVER['HTTPS'] empty when not available, IIS sets it to 'off'.
 * 'false' and 'disabled' are just guessing
 *
 * @returns bool true when SSL is active
 */
function is_ssl(){
	if (!isset($_SERVER['HTTPS']) ||
	preg_match('/^(|off|false|disabled)$/i',$_SERVER['HTTPS'])){
		return false;
	}else{
		return true;
	}
}
// From Word Press

Wbrew pozorom w PHP – CLI (Command Line Interface) – przydaje się nie tylko do tzw. scaffoldingu. Choć zaprezentowana poniżej funkcja pochodzi z instalatora Symfony, wierzcie mi, że nie trudno znaleźć powód do tego by uruchomić skrypt z linii komend.

/**
 * Check if PHP using CLI interface
 *
 * @return  boolean
 */
function is_cli()
{
    return !isset($_SERVER['HTTP_HOST']);
}

PHP CLI jest proste w użyciu i dla średnio obeznanego programisty PHP, który nie radzi sobie z bashem, albo zwyczajnie używa Windowsa umożliwia w szybki i prosty sposób własnoręczne stworzenie sobie przydatnych narzędzi operujących np. na wielu plikach itp. W prawdziwej webowej aplikacji też się znajdzie zastosowanie dla PHP CLI jak choćby skrypt do wysyłania newsletterów uruchamiany z Crona czy też spider chodzący po stronach i zbierający adresy email – no do tego to bym akurat użył Pythona, ale jak ktoś nie ma na serwerze Pythona czy też innego rozsądnego języka do dyspozycji to PHP CLI mu wystarczy.

Typ interfejsu możemy w PHP sprawdzić przy pomocy specjalnie do tego stworzonej funkcji php_sapi_name i tak dla przykładu dla CLI wyglądało by to tak:

function is_cli()
{
    return php_sapi_name() === 'cli';
}

Może to nawet lepsze rozwiązanie niż to użyte przez programistów Symfony, ale tamto też działa. W każdym razie niekiedy warto wiedzieć czy PHP pracuje np. w trybie CGI choć przyznam – nie miałem dotąd takiej potrzeby.

 
No Comments

Posted in PHP

 

jsTree – context content

29 gru

Skryptów do wizualizacji struktur drzewiastych nie brakuje. W javascript-cie powstało tego cała masa. Ja preferuje rozwiązania oparte o jquery i nawet po przyjęciu tego zawężającego kryterium mam w czym wybierać. To też wypróbowałem parę rozwiązań i ostatecznie zdecydowałem się na jstree.

Po przejrzeniu wszystkich dem prezentujących możliwości tej aplikacji (Piszę aplikacji bo coś co ma np. pluginy – nie jest już prostym skryptem), a także dokumentacji jestem naprawdę pod wrażeniem. Rozwijanie i zwijanie katalogów to banał, ale do tego dochodzi możliwość przeciągania elementów lub nawet całych gałęzi, możliwość dodawania, edytowania, usuwania, kopiowania elementów. Wspomniany skrypt jest naprawdę dobrze napisany. Daje możliwość przechwycenia wszelkich zachodzących na drzewie wydarzeń i dowolnego ich obsłużenia. Łącząc takie technologie jak ajax i np. php możemy doczytywać rozwijane katalogi lub też zapisywać wszelkie wykonane na drzewie operacje takie jak utworzenie nowego elementu lub też przeniesienie gałęzi w inne miejsce.

Wspomniałem już o pluginach. Do dyspozycji mamy kilka przydatnych rozszerzeń. „Cookie plugin” np. umożliwia zapisanie stanu drzewa. Dzięki temu po przeładowaniu strony wszystkie rozwinięte wcześniej gałęzie nadal takimi pozostają i nie musimy bawić się w ponowne klikanie by dojść do elementu na trzecim poziomie. „Keyboard navigation” umożliwia poruszanie się po drzewie przy pomocy klawiatury, a „Checkbox plugin” umożliwia zaznaczenie więcej niż jednego elementu poprzez kliknięcie utworzonego przy nazwie elementu checkboxa.

Jednym z ciekawszych dodatków jest w pełni konfigurowalne menu kontekstowe. „Context menu plugin” ma już predefiniowane ustawienie umożliwiające wykonanie takich akcji jak utworzenie podelementu oraz usunięcie lub edycja elementu bieżącego. Demo dodatkowo prezentuje w jaki sposób dodać do niego własną akcję. Menu kontekstowe ma jedną wadę, która objawia się w operze. Ponieważ pojawia się po kliknięciu na prawym przycisku myszki w Operze menu kontekstowe nie zadziała gdyż wspomniana przeglądarka nie daje możliwości nadpisania akcji prawokliku. Z tego też powodu zdecydowałem się nieco zmodyfikować tenże plugin w ten sposób, że po wybraniu danego elementu przy jego nazwie pojawia się mały plusik, którego kliknięcie otwiera menu kontekstowe.

(function ($) {
	$.extend($.tree.plugins, {
		"contextmenu" : {
			context_menu : {
				object : $("<ul id='jstree-contextmenu' class='tree-context' />"),
					data : {
					t : false,
					a : false,
					r : false
				},
				isrgtclick: false,
				defaults : {
					class_name : "hover",
					items : {
						create : {
							label	: "Create", 
							icon	: "create",
							visible	: function (NODE, TREE_OBJ) { if(NODE.length != 1) return 0; return TREE_OBJ.check("creatable", NODE); }, 
							action	: function (NODE, TREE_OBJ) { TREE_OBJ.create(false, TREE_OBJ.get_node(NODE[0])); },
							separator_after : true
						},
						rename : {
							label	: "Rename", 
							icon	: "rename",
							visible	: function (NODE, TREE_OBJ) { if(NODE.length != 1) return false; return TREE_OBJ.check("renameable", NODE); }, 
							action	: function (NODE, TREE_OBJ) { TREE_OBJ.rename(NODE); } 
						},
						remove : {
							label	: "Delete",
							icon	: "remove",
							visible	: function (NODE, TREE_OBJ) { var ok = true; $.each(NODE, function () { if(TREE_OBJ.check("deletable", this) == false) ok = false; return false; }); return ok; }, 
							action	: function (NODE, TREE_OBJ) { $.each(NODE, function () { TREE_OBJ.remove(this); }); } 
						}
					}
				},
				show : function(obj, t, e) {
					var opts = $.extend(true, {}, this.defaults, t.settings.plugins.contextmenu);
					obj = $(obj);
					if(obj.size() == 0) return;
					this.data.t = t;
					if(!obj.children("a:eq(0)").hasClass("clicked")) {
						this.data.a = obj;
						this.data.r = true;
						obj.children("a").addClass(opts.class_name);
						if (e) {
							e.target.blur();
						}
					}
					else { 
						this.data.r = false; 
						this.data.a = (t.selected_arr && t.selected_arr.length > 1) ? t.selected_arr : t.selected;
					}
 
					this.object.empty();
					var str = "";
					var cnt = 0;
					for(var i in opts.items) {
						if(!opts.items.hasOwnProperty(i)) continue;
						if(opts.items[i] === false) continue;
						var r = 1;
						if(typeof opts.items[i].visible == "function") r = opts.items[i].visible.call(null, this.data.a, t);
						if(r == -1) continue;
						else cnt ++;
						if(opts.items[i].separator_before === true) str += "<li class='separator'><span>&nbsp;</span></li>";
						str += '<li><a href="#" rel="' + i + '" class="' + i + ' ' + (r == 0 ? 'disabled' : '') + '">';
						if(opts.items[i].icon) str += "<ins " + (opts.items[i].icon.indexOf("/") == -1 ? " class='" + opts.items[i].icon + "' " : " style='background-image:url(\"" + opts.items[i].icon + "\");' " ) + ">&nbsp;</ins>";
						else str += "<ins>&nbsp;</ins>";
						str += "<span>" + opts.items[i].label + '</span></a></li>';
						if(opts.items[i].separator_after === true) str += "<li class='separator'><span>&nbsp;</span></li>";
					}
					var tmp = obj.children("a:visible").offset();
					this.object.attr("class","tree-context tree-" + t.settings.ui.theme_name.toString() + "-context").html(str);
					var h = this.object.height();
					var w = this.object.width();
					var x = tmp.left;
					var y = tmp.top + parseInt(obj.children("a:visible").height()) + 2;
					var max_y = $(window).height() + $(window).scrollTop();
					var max_x = $(window).width() + $(window).scrollLeft();
					if(y + h > max_y) y = Math.max( (max_y - h - 2), 0);
					if(x + w > max_x) x = Math.max( (max_x - w - 2), 0);
					this.object.css({ "left" : (x), "top" : (y) }).fadeIn("fast");
 
					if (e) {
						e.preventDefault(); 
						e.stopPropagation();
						this.isrgtclick = true;
					}
				},
				hide : function (check_isrgtclick) {
					if (check_isrgtclick == true && this.isrgtclick == false) {
						return;
					}
					this.isrgtclick = false;
					if(!this.data.t) return;
					var opts = $.extend(true, {}, this.defaults, this.data.t.settings.plugins.contextmenu);
					if(this.data.r && this.data.a) {
						this.data.a.children("a, span").removeClass(opts.class_name);
					}
					this.data = { a : false, r : false, t : false };
					this.object.fadeOut("fast");
				},
				exec : function (cmd) {
					if($.tree.plugins.contextmenu.context_menu.data.t == false) return;
					var opts = $.extend(true, {}, $.tree.plugins.contextmenu.context_menu.defaults, $.tree.plugins.contextmenu.context_menu.data.t.settings.plugins.contextmenu);
					try { opts.items[cmd].action.apply(null, [$.tree.plugins.contextmenu.context_menu.data.a, $.tree.plugins.contextmenu.context_menu.data.t]); } catch(e) { };
				},
				add : function (n, t) {
					if ($('#jstree-show-contextmenu').length == 0) {
						$(n).children('a').after('<a id="jstree-show-contextmenu" href="#">+</a>');
						$('#jstree-show-contextmenu').click(function(){
							$.tree.plugins.contextmenu.context_menu.show(n, t);
						});
					}
				},
				rem : function () {
					if ($('#jstree-show-contextmenu').length > 0) {
						$('#jstree-show-contextmenu').remove();
					}
				}
			},
			callbacks : {
				oninit : function () {
					if(!$.tree.plugins.contextmenu.css) {
						var css = '#jstree-contextmenu { display:none; position:absolute; z-index:2000; list-style-type:none; margin:0; padding:0; left:-2000px; top:-2000px; } .tree-context { margin:20px; padding:0; width:180px; border:1px solid #979797; padding:2px; background:#f5f5f5; list-style-type:none; }.tree-context li { height:22px; margin:0 0 0 27px; padding:0; background:#ffffff; border-left:1px solid #e0e0e0; }.tree-context li a { position:relative; display:block; height:22px; line-height:22px; margin:0 0 0 -28px; text-decoration:none; color:black; padding:0; }.tree-context li a ins { text-decoration:none; float:left; width:16px; height:16px; margin:0 0 0 0; background-color:#f0f0f0; border:1px solid #f0f0f0; border-width:3px 5px 3px 6px; line-height:16px; }.tree-context li a span { display:block; background:#f0f0f0; margin:0 0 0 29px; padding-left:5px; }.tree-context li.separator { background:#f0f0f0; height:2px; line-height:2px; font-size:1px; border:0; margin:0; padding:0; }.tree-context li.separator span { display:block; margin:0px 0 0px 27px; height:1px; border-top:1px solid #e0e0e0; border-left:1px solid #e0e0e0; line-height:1px; font-size:1px; background:white; }.tree-context li a:hover { border:1px solid #d8f0fa; height:20px; line-height:20px; }.tree-context li a:hover span { background:#e7f4f9; margin-left:28px; }.tree-context li a:hover ins { background-color:#e7f4f9; border-color:#e7f4f9; border-width:2px 5px 2px 5px; }.tree-context li a.disabled { color:gray; }.tree-context li a.disabled ins { }.tree-context li a.disabled:hover { border:0; height:22px; line-height:22px; }.tree-context li a.disabled:hover span { background:#f0f0f0; margin-left:29px; }.tree-context li a.disabled:hover ins { border-color:#f0f0f0; background-color:#f0f0f0; border-width:3px 5px 3px 6px; }';
						$.tree.plugins.contextmenu.css = this.add_sheet({ str : css });
					}
				},
				onrgtclk : function (n, t, e) {
					$.tree.plugins.contextmenu.context_menu.show(n, t, e);
				},
				onselect : function(n, t) {
					$.tree.plugins.contextmenu.context_menu.rem();
					$.tree.plugins.contextmenu.context_menu.add(n, t);
				},
				ondeselect : function(n, t) {
					$.tree.plugins.contextmenu.context_menu.rem();
				},
				onchange : function () { 
					$.tree.plugins.contextmenu.context_menu.hide(true);
				},
				beforedata : function () {
					$.tree.plugins.contextmenu.context_menu.hide(true);
				},
				ondestroy : function () {
					$.tree.plugins.contextmenu.context_menu.hide(true);
				}
			}
		}
	});
	$(function () {
		$.tree.plugins.contextmenu.context_menu.object.hide().appendTo("body");
		$("#jstree-contextmenu a")
			.live("click", function (event) {
				if(!$(this).hasClass("disabled")) {
					$.tree.plugins.contextmenu.context_menu.exec.apply(null, [$(this).attr("rel")]);
					$.tree.plugins.contextmenu.context_menu.hide();
				}
				event.stopPropagation();
				event.preventDefault();
				return false;
			})
		$(document).bind("mousedown", function(event) { if($(event.target).parents("#jstree-contextmenu").size() == 0) $.tree.plugins.contextmenu.context_menu.hide(); });
	});
})(jQuery);

Jeśli ktoś chce aby plusik pojawiał się tylko w Operze musi nieco zmodyfikować funkcje onselect i ondeselect dodając stosowny warunek. Przypominam, że jquery umożliwia w łatwy sposób identyfikację przeglądarki, oczywiście jeśli przedstawia się „prawdziwym imieniem”.

Idąc dalej śladem zaspokajania własnych potrzeb napisałem też swój plugin. Nazwałem go zgodnie z przyjętą zasadą „Context conten” i jak się można domyślić służy on do wyświetlania określonej treści powiązanej z danym elementem. W moim konkretnym przypadku miał służyć do wyświetlania podpowiedzi pod elementem w chwili jego wybrania.

(function ($) {
	$.extend($.tree.plugins, {
		"contextcontent" : {
			contenxt_content: {
				class_name: 'jstree-contextcontent',
				show: function(NODE, TREE_OBJ) {
					var id = $(NODE).attr('id');
					var cls = this.class_name;
					$('#'+id+' > .'+cls).show();
					$('#'+id+' > .'+cls+' a').click(function() {
						window.location.href = $(this).attr('href');
					});
				},
				hide : function (NODE, TREE_OBJ) {
					var id = $(NODE).attr('id');
					var cls = this.class_name;
					$('#'+id+' > .'+cls).hide();
				}
			},
 
			callbacks : {
				oninit : function (TREE_OBJ) {
					if (TREE_OBJ.settings.plugins.contextcontent.class_name) {
						$.tree.plugins.contextcontent.contenxt_content.class_name = TREE_OBJ.settings.plugins.contextcontent.class_name;
					}
					var cls = $.tree.plugins.contextcontent.contenxt_content.class_name;
					if(!$.tree.plugins.contextcontent.css) {
 
						var css = '.'+cls+' { display: none;}';
						$.tree.plugins.contextcontent.css = this.add_sheet({ str : css });
					}
				},
				onselect : function (NODE, TREE_OBJ) {
					$.tree.plugins.contextcontent.contenxt_content.show(NODE, TREE_OBJ);
				},
				ondeselect : function(NODE, TREE_OBJ) {
					$.tree.plugins.contextcontent.contenxt_content.hide(NODE, TREE_OBJ);
				}
			}
		}
	});
})(jQuery);

Aby go właściwie użyć trzeba:

1. Dołączyć skrypt do dokumentu html

<script type="text/javascript" src="jquery.js" charset="utf-8"></script>
<script type="text/javascript" src="jsTree/jquery.tree.js"></script>
<script type="text/javascript" src="jsTree/plugins/jquery.tree.contextcontent.js"></script>

2. Dodać do configa jstree stosowny wpis

$(function () {
	$("#demo").tree({
		plugins : {
			contextcontent : {
				class_name: 'jstree-contextcontent'
			}
		}
	});
});

3. No i utworzyć stosowny kod html. Proszę zwrócić uwagę na divy z atrybutem class „jstree-contextcontent”. Nazwę klasy oczywiście można zmienić pod warunkiem, że zrobi się to również w configu.

<div id="demo">
<ul>
	<li id="phtml_1" class="open">
		<a href="#"><ins>&nbsp;</ins>Root node 1</a>
		<div class="jstree-contextcontent">Root node 1 tip...</div>
		<ul>
			<li id="phtml_2">
				<a href="#"><ins>&nbsp;</ins>Child node 1</a>
				<div class="jstree-contextcontent">Child node 1 tip...</div>
			</li>
			<li id="phtml_3"><a href="#">
				<ins>&nbsp;</ins>Child node 2</a>
				<div class="jstree-contextcontent">Child node 2 tip...</div>
			</li>
			<li id="phtml_4">
				<a href="#"><ins>&nbsp;</ins>Some other child node with longer text</a>
				<div class="jstree-contextcontent">Some other child node with longer text tip...</div>
			</li>
		</ul>
	</li>
	<li id="phtml_5"><a href="#">
		<ins>&nbsp;</ins>Root node 2</a>
		<div class="jstree-contextcontent">Root node 1 tip...</div>
	</li>
</ul>
</div>
 
 

Przeskalowanie obrazków w katalogu

20 lis

Pracując nad funkcjonalnością uploadu plików z możliwością ich wcześniejszego podglądu postanowiłem, że pliki nie będące obrazkami będą prezentowane za pomocą ikon odzwierciedlających ich rozszerzenie.

Ściągnąłem sobie z deviantarta zestaw przykładowych ikon. Bardzo fajne, kolorowe i duże obrazki. Stanąłem przed potrzebą przeskalowania wszystkich ikonek. Pierwsza myśl to poszukać przeglądarki zdjęć z możliwością masowej edycji plików. Chwilę później przypomniałem sobie, że mam zainstalowanego image magicka. 5 min i miałem już napisany prosty skrypcik w bashu, który tworzy podkatalog na miniatury, wyszukuje w bieżącym katalogu wszystkie pliki o zadanym rozszerzeniu i tworzy ich przeskalowane kopie.

#!/bin/sh
WIDTH=100
HEIGHT=50
EXT=png
THUMBDIR=./thumbnails-${WIDTH}x${HEIGHT}
 
mkdir $THUMBDIR
 
for IMG in `ls *.${EXT}`
do
	convert -resize ${WIDTH}x${HEIGHT} $IMG ${THUMBDIR}/$IMG
done
 
No Comments

Posted in Bash