Sklep internetowy

Listopad okazał się bardzo pracowitym miesiącem. Nie miałem właściwie wolnego czasu dla siebie. Patrząc jednak na owoc programistycznych zmagań czuję się usatysfakcjonowany. Napisany przeze mnie skrypt sklepu internetowego spełnia swoją rolę. Co ważne oferuje funkcjonalność, której próżno szukać w gotowych produktach. Jako freelancer często spotykam się ze zleceniami, które wymagają autorskich rozwiązań. Sama oprawa graficzna strony została mi dostarczona i niestety miałem niewielki wpływ na design oraz nawigację. Galeria Qarat.

Galeria

02 08 2009
Opublikowano w Programowanie  |  4 Komentarzy
Tagi: , , , , ,

Co potrafi GD

GD jest biblioteką umożliwiającą pracę z grafiką z poziomu języka PHP. Posiada szereg możliwości począwszy od skalowania, kadrowania, filtrowania i skończywszy na zapisie w różnych formatach. A co z animacjami? Krucho. Po prostu nie zostały zaimplementowane. Ale głowa do góry jest proste rozwiązanie.

GIFEncoder

Brak obsługi animacji w bibliotece GD może okazać się czasami ograniczający. Naprzeciw z pomocą wychodzi dodatkowa klasa GIFEncoder. Specyfikacja formatu umożliwiła autorowi stworzenie programu łączącego wiele grafik (*.gif) w jeden animowany obrazek. Możemy zarówno wykorzystać pliki zapisane na dysku jak i te stworzone dynamicznie.

Klasę można pobrać z serwisu PHP Classes. Jako że rejestracja jest zniechęcająca oto i alternatywne źródło. Poniżej zamieszczam przykład użycia.

$frames = array();
$framed = array();

$colors = array(array(255,0,0),array(0,0,255));
foreach ($colors as $color)
{
    ob_start();
    $image = ImageCreate(50, 50); //width, height
    $temp = ImageColorAllocate($image,$color[0] ,$color[1] ,$color[2]);
    ImageFill($image,1,1,$temp);
    imagegif($image);
    imagedestroy($image);
    $frames[] = ob_get_clean();
    $framed[] = 100;//time
}

$gif = new GIFEncoder($frames,$framed,0,2,0, 0, 0,0,"bin");
Header('Content-type:image/gif');
echo $gif->GetAnimation();


Mój własny animowany baner

Wykorzystując opisaną klasę stworzyłem baner promujący moją stronę. Ważna była dla mnie estetyczna prosta szata graficzna. Nie ma fajerwerków. Jest to celowy zabieg. Nie chciałem dekoncentrować czytelników. Do tchnięcia życia w całość wystarczy skromna animacja. Na banerze umieszczam losowy wpis z bloga co ma odszukać potencjalnych zainteresowanych. Całość jest buforowania i aktualizuje się raz na 12 godzin.

31 07 2009
Opublikowano w Programowanie  |  1 Komentarzy
Tagi: , , ,

Hierarchiczna struktura

Zaimplementowanie drzewiastej struktury z wykorzystaniem baz danych takich jak MySQL czy PostgreSQL nie jest trudnym zadaniem. Już po chwili zastanowienia przychodzi nam na myśl koncepcja, w której każdy element wskazuje na swojego rodzica. W implementacji tego drzewa potrzebujemy tylko dwa dodatkowe pola: idparent_id. Przy wczytywaniu całości posługujemy się rekurencją. Skoro mamy tak przystępne rozwiązanie problemu czy warto poszukiwać czegoś innego? Jak najbardziej. O tym właśnie sobie pomówimy.

Można szybciej

Tradycyjne podejście ma jedną zasadniczą wadę. Zmusza nas abyśmy wykonali wiele zapytań do systemu bazodanowego. W prostych przypadkach jest to do zaakceptowania ale im większe mamy drzewo tym bardziej odczujemy skutki takiego wyboru. Z prostą reorganizacją zapisu drzewa możemy sytuację radykalnie poprawić.

Na rysunku zaprezentowałem drzewo z dodatkowymi parametrami leftright. Jeśli dany węzeł nie posiada potomka to jego wartość right jest większa od left o 1. Jeśli węzeł jest rodzicem jego parametr right musi być wystarczająco duży by pomieścić potomków. Dzięki takiej strukturze jesteśmy w stanie “poprosić” bazę danych o wszystkich potomków danej gałęzi za sprawą jednego zapytania.

SELECT * FROM `tree` WHERE `left` BETWEEN 1 AND 10 ORDER BY `left` ASC;

W wyniku zapytania otrzymujemy już posortowane dane. Możemy je wykorzystać w celu wyświetlenia drzewa. Poniżej prezentuję przykładowy kod PHP.

//$result - wynik zapytania
$right = array();//stos
foreach($result as $item)
{
    if(count($right) > 0)
        while($right[count($right)-1] < $item->right)
            array_pop($right);
    echo str_repeat('| ',count($right)).'<br/>';
    if(count($right) - 1 > 0)
        echo str_repeat('| ',count($right) - 1).'+- '.$item->name.'<br/>';
    else
        echo '+- '.$item->name.'<br/>';
    $right[] = $item->right;
}

Nie ma róży bez kolców. Nasze metoda jest bardzo szybka podczas czytania drzewa. Sama modyfikacja (przeniesienie/dodanie/usunięcie elementu) wymaga jednak sporego nakładu pracy. Każdemu z węzłów musimy zaktualizować parametry leftright. Ze swojego doświadczenia mogę zaproponować przechowywanie pomocniczego pola parent_id. Wtedy wszystkie czynności związane z przebudową są dużo prostsze w implementacji. Znając samą idee wierzę że jesteś w stanie napisać w tym celu odpowiednie funkcje samodzielnie.

Klasa Tree

Aby ułatwić sobie życie napisałem klasę upraszczającą korzystanie z drzewa. Zastosowałem w niej opisaną metodę. Zależało mi nie tylko na optymalizacji ale i elastyczności klasy. W wielu miejscach zrezygnowałem z przyśpieszenia kodu na rzecz wygody narzędzia. Klasa jest kompatybilna z Kohaną. Nie obsługuje jednak ORM. Jeśli zależy Ci na tej funkcjonalności polecam inne kompleksowe rozwiązanie.

Klasę dodajemy do naszego projektu wraz z przeniesieniem pliku Tree.php do katalogu/application/libraries/.

Publiczne metody:

  • hasDescendants() – sprawdza czy węzeł posiada potomków.
  • getChildren() – pobiera listę dzieci.
  • getParent() – pobiera rodzica. Jeśli go nie ma zwraca false.
  • getRoot() – pobiera korzeń.
  • getDepth() – sprawdza jak głęboko znajduje się dany węzeł. Zwraca liczbę całkowitą.
  • getPath($reverse=false) – pobiera tablicę elementów prowadzących do danego węzła. Kolejność może zostać odwrócona (domyślnie false).
  • getNodeById($id) – odszukuje węzeł o podanym identyfikatorze. Przeszukiwana jest lista potomków danego węzła.
  • move($item,$order=0,$parent=-1) – przenosi lub zmienia kolejność węzła. Metodę należy aktywować na korzeniu drzewa (id=parent_id). Argument $item oznacza przenoszoną gałąź. Jeśli parametr $item podamy jako liczbę uprzednio zostanie odszukany węzeł. Argument $order oznacza nową kolejność w danym rodzicu. Ostatni parametr umożliwia zmianę rodzica(domyślnie zasugerowany jest ten sam).
  • insertItem($parent=-1) – dodajemy nowy węzeł. Domyślnie w korzeniu. Możemy manipulować miejscem pojawienia się dziecka za sprawą argumentu $parent.
  • removeItem($item) – kolejna metoda obok moveinsertItemupdate, którą uruchamiamy dla korzenia. Parametr $item oznacza usuwany element.
  • update($item) – zapisuje zmodyfikowane wartości podanego w argumencie węzła.

Klasa Tree posiada jeszcze jedną bardzo ważną właściwość: protected $table=”";. Jest to zmienna określająca tabelę, z której zostanie wczytane drzewo. Wraz z dziedziczeniem klasy nadpiszemy tą wartość.

Wspomnijmy o samej tabeli. Narzuciłem konieczność istnienia pól: idleftrightparent_id. Co jeśli chcesz mieć dodatkowe wartości? Żaden problem. Po prostu je dodaj klasa sama zadba o ich wczytywanie oraz zapisywanie. Przypominam że wszystkie zmiany na drzewie wykonujemy względem korzenia. Ten cechuje się id=parent_id.

Pora zająć się samym modelem dziedziczącym po naszej klasie Tree. Model Categoriesbędzię czytał tabelę o tej samej nazwie. Ponadto rozszerzy funkcjonalność drzewa o dwie dodatkowe metody: prepareArrayprepareJson.

class Categories_Model extends Tree
{
    protected $table = 'categories';

    public function prepareJson()
    {
        return json_encode($this->prepareArray());
    }

    public function prepareArray($step=0,$temp=NULL,$childNodes=NULL,$child=NULL)
    {
        if ($step==0)
        {
            $child = $this;
            $temp = array();
            $temp['draggable'] = false;
        }

        $temp['id'] = $child->id;
        $temp['text'] = $child->text;
        $temp['left'] = $child->left;
        $temp['right'] = $child->right;
        $temp['leaf'] = !$child->hasDescendants();
        $temp['cls'] = $child->hasDescendants() ? 'folder':'file';
        if ($child->hasDescendants())
        {
            $temp['children'] = array();
            $children = $child->getChildren();
            foreach ($children as $child)
            {
                $step++;
                $this->prepareArray(&$step,&$temp['children'][],$children,$child);
                $step--;
            }
        }
        if ($step==0)
            return array($temp);
    }
}

Kontroler obsługujący nasz model.

class Welcome_Controller extends Controller
{
    private $categories;

    public function __construct()
    {
        parent::__construct();

        $this->template = new View('template');
        $this->template->body = new View('welcome');
        $this->categories=new Categories_Model();//domyślnie wczytujemy węzeł o id=1
    }

    public function index()
    {
        $this->template->body->tree = $this->categories->prepareArray();
        $this->template->render(TRUE);
    }

    public function add($id)
    {
        $item = $this->categories->insertItem($id);
        $item->text = "nowy".$item->id;
        $this->categories->update($item);
        url::redirect(url::base());
    }

    public function remove($id)
    {
        $this->categories->removeItem($id);
        url::redirect(url::base());
    }

    public function up($id)
    {
        $this->categories->move($id,0);
        url::redirect(url::base());
    }
}

Widok wyświetlający drzewo (welcome).

<?= isset($text)?$text:""; ?>
<?php
function htmlFromTree($tree,$step=0)
{
    if ($step==0)
        echo '<ul id="adoptme">';
    if ($tree['leaf']==0)
    {
        echo '<li>'.$tree['text'].' ('.$tree['left'].','.$tree['right'].')';
        echo '<ul>';
        foreach ($tree['children'] as $item)
        {
            $step++;
            htmlFromTree($item,$step);
            $step--;
        }
        echo '</ul></li>';
    }
    else
        echo '<li>'.$tree['text'].' ('.$tree['left'].','.$tree['right'].')';
 
    if ($step==0)
        echo '</ul>';
}

if (isset($tree))
    htmlFromTree($tree[0]);
?>


Załączniki

Tree.php

Drzewo – Kohana

Powrót do pracy

części pierwszej napisaliśmy podstawowe elementy frameworka. Pora zająć się rozbudową naszego projektu. Do efektywnej pracy potrzebne są dodatkowe moduły zapewniające obsługę baz danych, sesji czy operacji wejścia. Poniżej opiszę je pokrótce.

Dodatkowe moduły

system/libraries/database.php
Moduł zapewniający obsługę baz danych. Zwrócone dane przekazywane są w postaci obiektów.

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

class database_core
{
    protected $config;

    public static function & instance()
    {
        static $instance;
        if(!isset($instance))
        {
            $instance = new database();
        }
        return $instance;
    }

    protected function __construct() {}

    public function connect($config=array())
    {
        $this->config=(object)$config;
        mysql_connect($this->config->host, $this->config->user, $this->config->password);
        @mysql_select_db($this->config->dbname);
        mysql_query("SET NAMES 'utf8'");
    }

    public function query($sql = '')
    {
        $result = mysql_query($sql);
        if(mysql_num_rows($result) > 0)
        {
            $temp = array();
            while($r = mysql_fetch_object($result))
              $temp[]=$r;
            return $temp;
        }
        else false;
    }

    public function execute($sql = '')
    {
        mysql_query($sql);
    }

    public function last_id()
    {
        return mysql_insert_id();
    }
}

system/libraries/session.php
Moduł tworzy sesję. Ponadto dostarcza metody umożliwiające zapisywanie, pobieranie oraz usuwanie wartości.

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

class session_core
{
    public static function & instance()
    {
        static $instance;
        if(!isset($instance))
        {
            $instance = new session();
            session_start();
        }
        return $instance;
    }
 
    protected function __construct() { }
   
    public function set($key,$value)
    {
        $_SESSION[$key]=$value;
    }

    public function get($key,$default=false)
    {
        if (isset($_SESSION[$key]))
            return $_SESSION[$key];
        else
            return $default;
    }

    public function delete($key)
    {
      unset($_SESSION[$key]);
    }
}

system/libraries/input.php
Moduł wejścia. Zapewnia dostęp do zmiennych $_POST, $_GET. Warto go wzbogacić o funkcje zarządzające bezpieczeństwem.

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

class input_core
{
    public static function & instance()
    {
        static $instance;
        if(!isset($instance))
        {
            $instance = new input();
        }
        return $instance;
    }

    public function post($key)
    {
        return $_POST[$key];
    }

    public function get($key)
    {
        return $_GET[$key];
    }
}


Status

Mamy do dyspozycji już wystarczająco kompletne narzędzie by wykonać całą stronę internetową. Zgodnie z tradycją udostępniłem przykład w załączniku. Dalszych inspiracji w rozbudowie frameworka polecam poszukiwać również w innych projektach. Napiszę na ten temat coś jeszcze jeśli spotkam się z zainteresowaniem.

Galeria

Załączniki

Przykładowa strona internetowa

Wstęp

Oto pierwsza część z serii artykułów poświęconych budowie własnego frameworka. Materiał przygotowałem dla wszystkich osób, które rozpoczęły już przygodę z programowaniem w PHP ale brakuje im jeszcze wyrobionego warsztatu pracy. Tworząc skrypt zaczerpnąłem inspiracji z projektu Kohana. Teorię starałem się ograniczyć do minimum. Uznałem jednak za stosowne wspomnieć o wzorcach projektowych, które stanowią tutaj niemal najważniejszy element. W części pierwszej napiszemy jądro naszego systemu. Życzę miłej lektury.

Wzorce projektowe

Wzorce projektowe (ang. design pattern) opisują uniwersalne rozwiązania często spotykanych problemów projektowych. Ich użycie upraszcza tworzenie kodu źródłowego oraz poprawia jego czytelność. Wzorcom towarzyszy programowanie obiektowe.

Singleton – jeden z najpopularniejszych wzorców projektowych. Jego zadaniem jest ograniczenie możliwości tworzenia obiektu do jednej instancji. Singleton posiada pole statyczne będące wskaźnikiem na niego samego.

class Singleton
{
    private $data=1;

    public static function & instance()
    {
        static $instance;
        if(!isset($instance))
        {
            $instance = new Singleton;
        }
        return $instance;
    }

    protected function __construct() {}

    public function set($data)
    {
        $this->data=$data;
    }

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

$A = Singleton::instance();
$A->set(2);
echo $A->get();
echo ' ';
$B = Singleton::instance();
echo $B->get();
//W wyniku działania skryptu otrzymamy: "2 2".

MVC (Model-View-Controller) – wzorzec projektowy z podziałem zadań, w którym:

  • Model zajmuje się pracą z danymi. Zawiera metody, które upraszczają pobranie (przetwarzanie) danych. Przykładowo model Users może mieć metodę get zwracającą listę użytkowników. Programista nie musi przy tym znać zapytań użytych w modelu tylko obowiązujące w nim metody. Inne przykładowe modele: ProductsCategoriesPages,News.
  • Widok zajmuje się (tylko i wyłącznie) wyświetlaniem danych.
  • Kontroler jest obiektem zarządzającym całą aplikacją. Przykładowo: do pokazania news-ów kontroler pobiera dane z modelu oraz przekazuje je odpowiedniemu widokowi w celu wyświetlenia.
class News_Controller extends Controller
{
    public function index()
    {
        $news = new News_Model();
        $view = new View('news');
        $view->news = $news->get();
        $view->render();
    }
}


Piszemy nasz własny framework

W budowanym przez nas frameworku oddzielimy warstwę aplikacji od warstwy systemowej. Zachowamy dzięki temu czytelny podział. Struktura plików i katalogów wygląda następująco:

  • framework
    • application
    • system
      • core
        • core.php
        • uri.php
      • libraries
        • model.php
        • view.php
        • controller.php
    • index.php
    • .htaccess

index.php
Ładuje jądro naszego systemu.

require 'system/core/core.php';

.htaccess
Plik konfiguracyjny serwera Apache.

RewriteEngine On
RewriteBase /framework/
RewriteCond $1 ^(application|system)
RewriteRule ^(.*)$ index.php/access_denied/$1 [PT,L]
RewriteCond $1 ^(index\.php|data)
RewriteRule ^(.*)$ - [PT,L]
RewriteRule ^(.*)$ index.php/$1 [PT,L]

system/core/core.php
Jądro frameworka.

<?php
//definiujemy stałe
define('SYSPATH', 'system');
define('APPPATH', 'application');
define('DEFAULT_CONTROLLER', 'welcome');
define('DEFAULT_ACTION', 'index');

//final - klasa po której nie można dziedziczyć
final class FRAMEWORK
{
    public static $controller;

    public static function & instance()
    {
        static $instance;
        if(!isset($instance))
        {
            $instance = new UCEARN;
        }
        return $instance;
    }

    protected function __construct() {}

    public static function setup()
    {
        static $run;
        if ($run === TRUE) return;
        require SYSPATH.'/core/uri.php';
        //Uruchamiamy moduł URI
        URI::setup();
        //Zarejestrowanie autoloadera
        spl_autoload_register(array('FRAMEWORK', 'autoload'));
        $controller = URI::getController().'_controller';
        //utworzenie kontrolera
        self::$controller = new $controller();
        // tworzymy obiekt refleksji klasy
        $class  = new ReflectionClass(self::$controller);
        //pobieramy metodę
        $method = $class->getMethod(URI::getAction());
        //wykonanie metody z argumentami URI
        $method->invokeArgs(self::$controller,URI::getArguments());
        $run = TRUE;
    }

    private static function loadlibrary($class)
    {
        //wczytujemy bibliotekę
        require_once SYSPATH.'/libraries/'.$class.'.php';
        //rozszerzamy ją jeśli jest to możliwe, w przeciwnym wypadku stosujemy hack
        if (file_exists(URI::getPath().'/libraries/'.$class.'.php'))
            require_once URI::getPath().'/libraries/'.$class.'.php';
        else
            eval($extension = 'class '.$class.' extends '.$class.'_core { }');
    }

    public static function autoload($class)
    {
        //pobieramy typ obiektu
        if (($type = strrpos($class, '_')) !== FALSE)
        {
            $type = substr($class, $type + 1);
        }
        else
            $type = 'default';
        //dla danego typu
        switch($type)
        {
            case 'controller':
                self::loadlibrary('controller');
                $file = substr($class, 0, -11);
                if (file_exists(URI::getPath().'/controllers/'.$file.'.php'))
                    require_once URI::getPath().'/controllers/'.$file.'.php';
                else
                {
                    header('HTTP/1.1 404 File Not Found');
                    self::exception_handler(NULL,'[404] File not found');
                    exit;
                }
                break;
            case 'model':
                require_once SYSPATH.'/libraries/model.php';
                $file = substr($class, 0, -6);
                require_once URI::getPath().'/models/'.$file.'.php';
                break;
            case 'default':
                self::loadlibrary($class);
                break;
        }
    }
}
//uruchamiamy moduł
FRAMEWORK::setup();

system/core/uri.php
Moduł rozbijający adres strony na części: kontroler, akcja, argumenty.

final class URI
{
    protected static $pathinfo;
    protected static $array;
    protected static $controller;
    protected static $action;
    protected static $arguments;
    protected static $path;

    public static function & instance()
    {
        static $instance;
        if(!isset($instance))
        {
            $instance = new URI;
        }
        return $instance;
    }

    protected function __construct() { }

    public static function setup()
    {
        static $run;
        if ($run === TRUE) return;
        self::$pathinfo = $_SERVER['PATH_INFO'];
        self::$pathinfo = preg_replace('#\.[\s./]*/#', '', self::$pathinfo);
        self::$pathinfo = trim(self::$pathinfo, '/');
        //kontroler
        self::$array = explode('/',self::$pathinfo);
        if (!empty(self::$array[0]))
        {
            $temp = array_shift(self::$array);
            self::$controller = $temp;
            self::$path = APPPATH;
        }
        else
        {
            self::$controller = DEFAULT_CONTROLLER;
            self::$path = APPPATH;
        }
        //akcja
        if (!empty(self::$array[0]))
            self::$action = array_shift(self::$array);
        else
            self::$action = DEFAULT_ACTION;
        //argumenty
        self::$arguments = self::$array;
    }

    public static function getController()
    {
        return self::$controller;
    }

    public static function getAction()
    {
        return self::$action;
    }

    public static function getArguments()
    {
        return self::$arguments;
    }

    public static function getPath()
    {
        return self::$path;
    }
}

system/libraries/controller.php
Klasa kontrolera.

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

class controller_core
{
    public function __construct()
    {
        $this->uri = URI::instance();
    }
}

system/libraries/model.php
Klasa modelu.

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

class model_core
{
    public function __construct()
    {
    }
}

system/libraries/view.php
Klasa widoku.

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

class view_core
{
    protected $filename = FALSE;
    protected $data = array();

    public function __construct($name = NULL)
    {
        if (is_string($name) AND $name !== '')
        {
            $this->filename = $name;
        }
    }

    public function __toString()
    {
        return $this->render();
    }

    public function __set($key, $value)
    {
        if ( ! isset($this->$key))
        {
            $this->data[$key] = $value;
        }
    }

    public function __get($key)
    {
        if (isset($this->data[$key]))
            return $this->data[$key];
        if (isset($this->$key))
            return $this->$key;
    }

    protected function load_view($filename, $input_data)
    {
        if ($filename == '')
            return;
        //buforowanie wyjścia
        ob_start();
        //importowanie zmiennych z tablicy do bieżącej tablicy symboli
        extract($input_data, EXTR_SKIP);
        include $filename;
        //zwróć bufor
        return ob_get_clean();
    }

    public function render($print=FALSE)
    {
        $output = $this->load_view(URI::getPath().'/views/'.$this->filename.'.php', $this->data);
        if ($print==TRUE)
        {
            header('Content-Type: text/html; charset=UTF-8');
            echo $output;
            return;
        }
        return $output;
    }
}


To… dopiero początek

Dysponujemy już podstawowymi elementami frameworka. Za ich sprawą możemy wykonać naszą pierwszą aplikację (udostępniłem ją w archiwum). Do poważnej pracy potrzebujemy jednak czegoś jeszcze – dodatkowych modułów. Już wkrótce je napiszemy i dołączymy do projektu. Zapraszam.

Załączniki

Framework