Sortowanie tablicy wielowymiarowej w php

Zdecydowałem się wreszcie opublikować mój artykuł na temat wzorca projektowego DTO (Data Transfer Object), który napisałem już jakiś czas temu. Przy okazji parę akapitów poprawiłem i dopisałem nowe przykłady zastosowania. Przygotowując przykład dotyczący Recordset-u odkopałem przydatną funkcję ułatwiającą [sortowanie tablicy wielowymiarowej](http://php.net/manual/pl/function.array-multisort.php#87268). Jest uniwersalna i moim zdaniem warto zwrócić na nią uwagę. Ponad to wbrew pozorom nie musi służyć jedynie do sortowania wyników zapytań SQL-owych.

<?php
/**
 * 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)
 *
 * <code>
 * <?php
 * // You can sort data by several columns e.g.
 * $data = array();
 * for ($i = 1; $i <= 10; $i++) {
 *     $data[] = array( 'id' => $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');
 * print_r($data);
 * $data = sortDbResult($data, 'last_name', SORT_ASC, SORT_STRING, 'first_name', SORT_ASC, SORT_STRING);    
 * print_r($data);
 * ?>
 * </code>
 *
 * @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;
        $_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']];
                $_params[] = $_rule['order'];
                $_params[] = $_rule['mode'];
            }
            $_cols[$_rule['name']][$_k] = $_row[$_rule['name']];
        }
    }
    $_params[] = &$_data;
    call_user_func_array('array_multisort', $_params);
    return $_data;
}
?>

Posted in PHP by Zbigniew Heintze · Tags: ,

DTO – zastosowanie – Recordset, Rejestr, Menadżer zmiennych, Konfiguracja

Przed przeczytaniem tego artykułu zapoznaj się z wpisem DTO – wprowadzenie i charakterystyka.

Recordset

Wzorzec projektowy Data Transfer Object jest często używany np. w połączeniu z wzorcemi Data Access Objects i/lub Recordset. Obiekty Recordset są wykorzystywane do operowania danymi w bazie danych na poziomie rekordów. Nie wszyscy pamiętają, że pod pojęciem Bazy Danych może kryć się serwer MySQL, ale też pliki tekstowe, pliki XML itd. PHP ma świetne moduły do pracy z tradycyjnie rozumianymi Bazami Danych takie jak np. PDO. Wyciągając dane z kwerendy możemy otrzymać je zarówno w formie tablicy, obiektu, pogrupowane, posortowane itd. Pobierając dane z innych źródeł nie zawsze mamy taki komfort. Tymczaem standardowy obiekt DTO świetnie nadaje się jako podstawa konstrukcyjna uniwersalnego obiektu Recordset.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Recordset extends bigWeb_Dto
{
    protected $_data;
 
    public function __construct(array $properties = array()) 
    {
       	if (!empty($properties)) {
    		foreach ($properties as $_key => $_value) {
    			if (is_array($_value)) {
    				$properties[$_key] = new bigWeb_Dto($_value);
    			}
    		}
    	}
        parent::__construct($properties);
    }
 
 
    public function multisort()
    {
    	$_args = func_get_args();
    	array_unshift($_args, $this->getArrayCopy());
    	$_result = call_user_func_array(
            array(get_class($this), '_sortDbResult'), $_args);
    	$this->exchangeArray($_result);
    }
 
    protected static function _sortDbResult(array $data ) 
    {
        $_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;
            $_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']];
                    $_params[] = $_rule['order'];
                    $_params[] = $_rule['mode'];
                }
                $_cols[$_rule['name']][$_k] = $_row[$_rule['name']];
            }
        }
        $_params[] = &$_data;
        call_user_func_array('array_multisort', $_params);
        return $_data;
    }
}

Obiekt Recordset oparty o naszą podstawową klasę DTO posada przede wszystkim wygodne mechanizmy dostępu do danych. Można do niego załadować dane pobrane z Bazy Danych (BD), jak równierz z każdego innego źródła lub też wygenerowane dynamicznie co przedstawia przykład poniżej. Dane pobierane z BD wygodniej jest sortować już na etapie konstruowania zapytania SQL-owego. Nie zawsze mamy taką możliwość dlatego też moja propozycja obiektu Recordset otrzymała uniwersalną metodę sortującą.

$data = array();
for ($i = 1; $i <= 10; $i++) {
    $data[] = array( "id" => $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()))
                 );
}
$ob = new bigWeb_Recordset($data);

Po wygenerowaniu danych testowych i załadowaniu do obiektu Recordset sortujemy dane po kolumnie „date” malejąco, a w drugiej kolejności po kolumnie „id” …

$ob->multisort("date", SORT_DESC, SORT_NUMERIC, "id");
/*
array
  0 => 
    array
      'id' => int 10
      'first_name' => string 'first_name_3' (length=12)
      'last_name' => string 'last_name_2' (length=11)
      'date' => string '2004-10-20' (length=10)
  1 => 
    array
      'id' => int 7
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_9' (length=11)
      'date' => string '2003-07-25' (length=10)
  2 => 
    array
      'id' => int 1
      'first_name' => string 'first_name_4' (length=12)
      'last_name' => string 'last_name_3' (length=11)
      'date' => string '1990-07-30' (length=10)
  3 => 
    array
      'id' => int 9
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1981-02-09' (length=10)
  4 => 
    array
      'id' => int 2
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_1' (length=11)
      'date' => string '1979-11-25' (length=10)
  5 => 
    array
      'id' => int 4
      'first_name' => string 'first_name_9' (length=12)
      'last_name' => string 'last_name_6' (length=11)
      'date' => string '1976-09-28' (length=10)
  6 => 
    array
      'id' => int 8
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1976-02-14' (length=10)
  7 => 
    array
      'id' => int 6
      'first_name' => string 'first_name_8' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1975-11-15' (length=10)
  8 => 
    array
      'id' => int 3
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1974-06-02' (length=10)
  9 => 
    array
      'id' => int 5
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1972-11-07' (length=10)
*/

… albo według nazwiska i imienia.

$ob->multisort(
    "last_name", SORT_ASC, SORT_STRING
    , "first_name", SORT_ASC, SORT_STRING);
/*
array
  0 => 
    array
      'id' => int 2
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_1' (length=11)
      'date' => string '1979-11-25' (length=10)
  1 => 
    array
      'id' => int 10
      'first_name' => string 'first_name_3' (length=12)
      'last_name' => string 'last_name_2' (length=11)
      'date' => string '2004-10-20' (length=10)
  2 => 
    array
      'id' => int 1
      'first_name' => string 'first_name_4' (length=12)
      'last_name' => string 'last_name_3' (length=11)
      'date' => string '1990-07-30' (length=10)
  3 => 
    array
      'id' => int 4
      'first_name' => string 'first_name_9' (length=12)
      'last_name' => string 'last_name_6' (length=11)
      'date' => string '1976-09-28' (length=10)
  4 => 
    array
      'id' => int 3
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1974-06-02' (length=10)
  5 => 
    array
      'id' => int 5
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1972-11-07' (length=10)
  6 => 
    array
      'id' => int 6
      'first_name' => string 'first_name_8' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1975-11-15' (length=10)
  7 => 
    array
      'id' => int 8
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1976-02-14' (length=10)
  8 => 
    array
      'id' => int 9
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1981-02-09' (length=10)
  9 => 
    array
      'id' => int 7
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_9' (length=11)
      'date' => string '2003-07-25' (length=10)
*/

Multisortowanie to tylko prosta demonstracja tego co możemy zrobić, aby ułatwić sobie pracę. Nic nie stoi na przeszkodzie aby do objektu Recordset rozbudować o dodatkowe funkcjonalności np. dodać metody umożliwiające zapis danych do szeroko rozumianej Bazy Danych i tym samym przekształcić go w „Active Record”.

Rejestr

W aplikacji zawsze pojawiają się takie mechanizmy, które mają charakter globalny. Istnieje wiele różnych rozwiązań problemu globalnego zasięgu określonych zmiennych. Jednym z prostszych, a zarazem dobrych rozwiązań zapanowania nad obiektami i wszelkimi innymi wartościami, do których konieczny jest dostęp z dowolnego miejsca aplikacji jest skorzystanie ze wzorca „Registry”. Rejestr daje dostęp do dowolnej zarejestrowanej w nim zmiennej pod warunkiem, że sam jest ogólnie dostępny. Globalny dostęp do rejestru łatwo jest zrealizować implementując wzorzec „Singletona”. Ponieważ jednak Singleton wymaga, aby konstruktor był prywatny, a przynajmniej chroniony nie możemy w tym wypadku skorzstać z prostego dziedziczenia po obiekcie DTO. Z pomocą przyjdzie nam wzorzec „Factory”.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Registry
{
    private static $_instance;
 
    private function __construct()
    {}
 
    public static function getInstance($properties = null) {
        if (is_null(self::$_instance)) {
        	self::$_instance = new bigWeb_Dto($properties);
        }
        return self::$_instance;
    }
}

Proste użycie plus dostęp z dowolnego miejsca aplikacji to w zasadzie wszystko czego nam potrzeba.

$registry = bigWeb_Registry::getInstance();
 
$dsn = "mysql:dbname=testdb;host=127.0.0.1";
$user = "dbuser";
$password = "dbpass";
 
$registry->db = new PDO($dsn, $user, $password);

Czy rzeczywiście wszystko? Rozbudowana aplikacja internetowa wykonuje szereg różnorakich akcji. Wykonanie zainicjalizowanych przez użytkownika zadań wymaga współdziałania różnych mechanizmów lecz nie zawsze tych samych. Nie zawsze potrzebny jest dostęp do bazy danych, nie zawsze konieczną jest też inicjalizacja obiektów związanych z warstwą widoku. W zasadzie stosunkowo rzadko potrzebujemy klasy, która wysyła maila. Zadajmy sobie pytanie. Jaki jest sens tworzenia tych wszystkich – często zbędnych – obiektów za każdym razem, kiedy użytkownik wpisze w pole adresowe przeglądarki jakiś url? Chcąc w efektywny sposób korzystać z zasobów nie powinniśmy na dzień dobry wywalać całej skrzynki z narzędziami, a jedynie sięgać po te z narzędzi, które pozwolą nam zrealizować bierzące zadanie. Dobrym pomysłem byłoby zaimplementowanie w Rejestrze mechanizmu leniwej inicjalizacji „Lazy initialization”, która powoła do życia obiekt dopiero w chwili gdy jakiś proces o niego poprosi.

Menadżer zmiennych

Właściwie nagminnie posługujemy się tablicami GET, POST, SESSION itp. co w praktyce często okazuje się niewygodne zwłaszcza kiedy nie wiemy do końca jakie zmienne zawiera tablica. Najbardziej uciążliwe są zwłaszcza testy sprwadzające czy dany indeks w ogóle istnieje w tablicy. Słowniki w pythonie posiadają metodę umożliwiającą pobranie danego elementu, a jeżeli on nie występuje zastąpienie go domyślną wartością. Tablice w PHP nie mają takiej funkcjonalności, ale prosty obiekt DTO możemy wzbogacić o podobną metodę.

Drugą z metod o jaką warto się pokusić w tym konkretnym przypadku jest funkcja umożliwiająca zastąpienie określonego zestawu wartości jaki przyjmuje dana zmienna na inny zestaw.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Vars extends bigWeb_Dto
{
 
    public function __construct($properties = null){
        parent::__construct($properties);
    }
 
    public function get($property, $default = null)
    {
        return $this->offsetExists($property) ? $this->offsetGet($property) : $default;
    }
 
    public function map($property, array $map = array(), $default = null)
    {
        if (!$this->offsetExists($property)) {
            return $default;
        }
        $_val = $this->offsetGet($property);
        return (array_key_exists($_val, $map)) ? $map[$_val] : $_val;
    }
}

Załużmy taki scenariusz. Mamy tablicę POST, którą otrzymaliśmy po zatwierdzeniu formularza.

$_POST = array(
	"id" => 10,
	"login" => null,
	"email" => "jan.kowalski@domain.com",
	"first_name" => "Jan",
	"last_name" => "Kowalski",
	"gender" => "m",
	"submit" => "OK");

W naszym formularzu było jeszcze pole status określające czy dany użytkownik jest aktywny, ale ponieważ było to pole typu checkboks i nie zostało ono zaznaczone dlatego w POST nie ma zmiennej „status”.

Chcemy nasze dane zapisać w bazie danych z tym, że jeżeli nie został podany login to należy w jego miejsce wpisać adres email. Z kolei zapisując płeć dla kobiety musimy ustawić „0” a dla mężczyzny „1” bo pole w bazie danych jest typu INT. Podobnie pole status też przyjmuje wartości „0” i „1”. Operujac na bezposrednio na tablicy POST musielibyśmy popełnić coś na wzór.

if (isset($_POST["submit"])) {
	$data = array(
		"id" => (int) @$_POST["id"],
		"nick" => (!empty($_POST["login"]) ? $_POST["login"] : @$_POST["email"]),
		"email" => @$_POST["email"],
		"first_name" => @$_POST["first_name"],
		"last_name" => @$_POST["last_name"],
		"gender" => (isset($_POST["gender"]) && $_POST["gender"] == "m") ? 1 : 0,
		"status" => (int) @$_POST["status"],
	);
	$db->save($data);
}

Maskowania błędów @ użyłem po to aby ukryć ewentualne ostrzeżenia. Chcąc to wykonać prawidłowo powinienem za każdym razem wykonać test z użyciem np. funkcji „isset”, ale wymagałoby to napisania jeszcze dłuższego kodu. Wybrałem rozwiązanie mniej eleganckie ale krótsze chcąc pokazać, że użycie obiektu DTO nie powoduje powstania dłuższego kodu, za to jest rozwiązaniem o wiele bardziej przejrzystym i eleganckim.

$post = new bigWeb_Dto_Vars($_POST);
if (isset($post->submit)) {
	$data = array(
		"id" => (int) $post->id,
		"nick" => $post->get("login", $post->email),
		"email" => $post->email,
		"first_name" => $post->first_name,
		"last_name" => $post->last_name,
		"gender" => $post->map("gender", array("m" => 1, "k" => 0), 0),
		"status" => (int) $post->staus,
	);
	$db->save($data);
}

Mając już menadżer zmiennych jesteśmy zaledwie o krok od zbudowania profesjonalnej klasy Request lub też narzędzia do zarządzania sesjami.

Konfiguracja

Projektując aplikację lub też konstruując framework szybko przekonasz się, o potrzebie wydzielenia danych konfiguracyjnych. Wraz z rozwojem aplikacji rozrasta się też jej część konfiguracyjna, która dodatkowo powoli zaczyna przyjmować kształt hierarchicznej struktury drzewiastej. Konfigurację można trzymać w plikach ini, xml, yaml lub w postaci tablic. Wybór formatu przechowywania to jedna sprawa, a zarządzanie i łatwosć dostępu do zmiennych to już zupełnie coś innego. Dążąc do maksymalnej wygody użycia przerobiłem nieco mój podstawowy obiekt DTO, dzięki czemu nabrał on nowych „cudownych” właściwości.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Dto_Recurrence extends bigWeb_Dto {
 
	protected $_isWritable;
 
	public  function __construct($properties = null)
	{
		$this->_isWritable = true;
		parent::__construct($properties);
		$this->setIsWritable(false);
	}
 
	public function setIsWritable($mode)
	{
		$this->_isWritable = (bool) $mode;
		$_class = get_class($this);
		foreach ($this as $_value) {
			if ($_value instanceof $_class) {
				$_value->setIsWritable($mode);
			}
		}
		return $this;
	}
 
	public function offsetSet($property, $value)
	{
		if (true !== $this->_isWritable) {
			return false;
		}
		$_class = get_class($this);
		if (is_array($value)) {
			$value = new $_class($value);
			$value->setIsWritable($this->_isWritable);
		}
		parent::offsetSet($property, $value);
		return true;
	}
 
	public function offsetGet($property)
	{
		$_value = parent::offsetGet($property);
		if (true === $this->_isWritable && is_null($_value)) {
			$this->offsetSet($property, array());
			return $this->offsetGet($property);
		}
		return $_value;
	}
 
	public function __isset($property)
	{
		$_value = parent::offsetGet($property);
		if (is_null($_value)) return false;
		if ($_value instanceOf ArrayObject) {
			if (count($_value) == 0) return false;
		}
		return true;
	}
 
	public function getArrayCopy()
	{
		$_array = parent::getArrayCopy();
		foreach ($_array as $_key => $_value) {
			if (is_array($_value) && empty($_value)) {
				unset($_array[$_key]);
			}
		}
		return $_array;
	}
 
	protected function _getRecursiveArrayCopy($array)
	{
		$_array = array();
		foreach ($array as $_key => $_value) {
			if ($_value instanceOf ArrayObject) {
				if (count($_value) === 0) {
					continue;
				} else {
					$_value = $this->_getRecursiveArrayCopy($_value->getArrayCopy());
					if (count($_value) === 1) {
						$_val = current($_value);
						if (empty($_val)) {
							continue;
						}
					}
				}
			}
			$_array[$_key] = $_value;
		}
		return $_array;
	}
 
	public function __clone()
	{
		$_isWritable = $this->_isWritable;
		$this->setIsWritable(true);
		foreach ($this as $_property => $_value) {
			$_class = get_class($this);
			if ($_value instanceof $_class) {
				$this->offsetSet($_property, clone $_value);
			}
		}
		$this->setIsWritable($_isWritable);
	}
}

Rekurencyjny obiekt DTO działa w dwóch trybach.

$config = new bigWeb_Dto_Recurrence(array(
	"database" => array(
		"host" => "localhost"
		, "pass" => "dbpass"
		, "user" => "dbuser"
		, "db" => "testdb"
	)));
 
$config->database->user;
// string 'dbuser' (length=6)
 
$config->database->pass;
// string 'dbpass' (length=6)
 
$config->database->host;
// string 'localhost' (length=9)
 
$config->database->db
// string 'testdb' (length=6)
 
$config->database->port;
// null

W domyślnym trybie do odczytu dane w obiekcie można zapisać jedynie w trakcie tworzenia nowej instancji obiektu poprzez przekazanie za pośrednictwem konstruktora.

$config->database->port = 25;
$config->database->port;
// null
 
$config["database"]["port"] = 25;
$config["database"]["port"];
// null

Włączając tryb do zapisu ujawnia się magia.

$config->setIsWritable(true);
 
$config->database->port = 25;
$config->database->port;
// int 25
 
$config->zmienna->na->dowolnym->poziomie->zagniezdrzenia = "foo";
$config->zmienna->na->dowolnym->poziomie->zagniezdrzenia;
// string 'foo' (length=3)

Powodem, dla którego zdecydowałem się wprowadzić tryb zapisu jest jedna nieporządana cecha, która się ujawnia właśnie w trybie do zapisu. Możemy się bezkarnie odwoływać do nieistniejącej zmiennej i nigdy nie otrzymamy wartości NULL tylko pusty obiekt DTO.

$config->to->jest->nieistniejaca->zmienna
/*
object(bigWeb_Dto_Recurrence)[15]
  protected '_isWritable' => boolean true
*/

Mając taką bazę wystarczy dopisać metodę pobierającą dane z pliku oraz, metodę zachowującą zmiany w pliku i otrzymujemy całkiem zaawansowany menadżer konfiguracji.

<?php
require_once 'bigWeb/Dto/Recurrence.php';
 
class bigWeb_Config extends bigWeb_Dto_Recurrence
{
	protected $_file;
 
	public function __construct($file)
	{
		if (is_string($file)) {
			$this->setFile($file);
			$_properties = include $file;
		} else {
			$_properties = $file;
		}
		parent::__construct($_properties);
	}
 
	public function setFile($file)
	{
		if (!file_exists($file)) {
			throw new bigWeb_Config_Exception('File "'.$file.'" does not exist');
		}
		$this->_file = $file;
		return $this;
	}
 
	public function save()
	{
		if (!$this->_isWritable) {
			return $this;
		}
		if (is_null($this->_file)) {
			throw new bigWeb_Config_Exception('File does not set');
		}
		if (!is_writable($this->_file)) {
			throw new bigWeb_Config_Exception('File "'.$this->_file.'" is not writable');
		}
		$_content = "<?php\nreturn ".var_export($this->getArrayCopy(), true).';';
		$_result = self::createOrReplaceFile($this->_file, $_content);
		if (false == $_result) {
			throw new bigWeb_Config_Exception('Data could not be save in file');
		}
		return $this;
	}
 
        static public function createOrReplaceFile($file, $content = null, $mode = 'w+', $mask = 0755)
	{
		$_path = dirname($file);
		if (!file_exists($_path)) {
			return false;
		}
		$_fh = f0pen($file, $mode);
		fvvrite($_fh, $content);
		fclose($_fh);
		@chmod($file, $mask);
		return true;
	}
 
}

**Uwaga!** Ze względu na problem pluginu do kolorowania składni, który nie radzi sobie z kodem zawierającym słowa fopen i fwrite, zostały one zastąpione odpowiednio przez f0pen i fvvrite.

DTO – wprowadzenie i charakterystyka

Wprowadzenie

Data Transfer Object w skrócie DTO jest wzorcem projektowym należącym do grupy wzorców dystrybucji. Podstawowym zadaniem DTO jest transfer danych pomiędzy systemami, aplikacjami lub też w ramach aplikacji pomiędzy warstwami aplikacji, modułami lub też w każdej dowolnej sytuacji, w której transfer danych jest konieczny. DTO – najprościej rzecz ujmując – jest bardziej rozbudowanym kontenerem na dane wzbogaconym o dodatkowe możliwości ułatwiające dostęp, ochronę oraz przesyłanie danych.

Dobrze skonstuowany DTO staje się użytecznym narzędziem jako taki lub też może stać się podstawą konstrukcyjną wielu elementów składających się na większy framework.

Dzięki klasie ArrayObject ze standardowej biblioteki SPL dostępnej w PHP w wersji 5 oraz metodom magicznym możemy zdefiniować klasę umożliwiającą powoływanie do życia obiektów mających zarówno wiele właściwości tablic, jak i obiektów.

<?php
class bigWeb_Dto extends ArrayObject implements Serializable {
    public function __construct($properties = null){
        parent::__construct(array(), ArrayObject::STD_PROP_LIST);
        if ($properties) {
            $this->append($properties);
        }
    }
 
    public function __get($property){
        return $this->offsetGet($property);
    }
 
    public function __set($property, $value) {
        return $this->offsetSet($property, $value);
    }
 
    public function __isset($property) {
        return parent::offsetExists($property);
    }
 
    public function __unset($property) {
        return $this->offsetUnset($property);
    }
 
    public function offsetGet($property) {
        return (parent::offsetExists($property)) 
            ? parent::offsetGet($property) : null;
    }
 
    public function offsetSet($property, $value) {
        parent::offsetSet($property, $value);
        return $this;
    }
 
    public function offsetUnset($property)
    {
        if (parent::offsetExists($property)) {
            parent::offsetUnset($property);
        }
        return $this;
    }
 
    public function append($properties) {
        if ((is_array($properties) || $properties instanceOf ArrayObject) 
            && !empty($properties))
        {
            foreach($properties as $_name => $_value) {
                $this->offsetSet($_name, $_value);
            }
        }
    }
 
    public function getArrayCopy() {
        return $this->_getRecursiveArrayCopy(parent::getArrayCopy());
    }
 
    protected function _getRecursiveArrayCopy($array) {
        $_array = array();
        foreach($array as $_key=>$_value) {
            if ($_value instanceOf ArrayObject) {
                $array[$_key] = $this->_getRecursiveArrayCopy(
                    $_value->getArrayCopy());
            }
        }
        return $array;
    }
 
    public function serialize() {
        return serialize($this->getArrayCopy());
    }
 
    public function unserialize($data) {
        $this->exchangeArray(unserialize($data));
    }
 
    public function __toString() {
        return sprintf('%s%s', 
            print_r($this, true), 
            print_r($this->getArrayCopy(), true));
    }
}

Klasa ta w swojej podstawowej formie jest bardziej rozbudowaną wersją tablicy, a jej użyteczność – nie licząc możliwości serializacji – polega głównie na stworzeniu kilku metod dostępu do danych. Jednakże, jest ona jedynie punktem wyjścia do budowy bardziej zaawansowanych rozwiązań daleko wykraczających poza możliwości zwykłych tablic. Zanim przedstawię potencjalne pola zastosowań stworzonego przeze mnie narzędzia chciałbym omówić kilka jego cech i właściwości.

Podstawowa charakterystyka

Utworzenie objektu i dodanie danych

Nową instancję można zainicjalizować przekazując do konstruktora istniejącą tablicę, ale można też utworzyć „pusty” objekt.

$dto = new bigWeb_Dto(array("a" => 1));

Dane w kontenerze można umieszczać wykorzystując składnię obiektów lub tablic. Każdej porcja danych ma swój indeks, dzięki któremu będzie można później te dane wyciągnąć z „opakowania”

$dto->b = 2;
$dto["c"] = 3;

Możliwe jest też obiema metodami ustawianie zmiennych o indeksach liczbowych, jednak chcąc użyć składni charakterystycznej dla obiektów konieczne jest w tym wypadku użycie nawiasów klamrowych.

$dto->{0} = 4;
$dto[1] = 5;

Analogicznie do tablic mamy też możliwość przypisania określonej wartości do pierwszego wolnego indeksu liczbowego.

$dto[] = 6;

Wyciąganie danych z obiektu

Mając już utworzony obiekt możemy wyciągnąć wartości przypisane do poszczególnych zmiennych. Także w tym wypadku mamy do dyspozycji zarówno sposób obiektowy, jak i tablicowy.

$dto->b;
// int 2
 
$dto["c"];
// int 3
 
$dto->{0};
// int 4
 
$dto[1];
//int 5

Interfejs obiektu ArrayObject przewiduje metodę „getArrayCopy” umożliwiającą sprowadzenie obiektu do postaci czystej tablicy. Dzięki czemu jeśli w jakimkolwiek momęcie użycie tablicy stanie się bardziej wygodne lub też użyteczne mamy możliwość operowania na tablicy, którą po przekształceniach można oczywiście znów zapakować do objektu DTO

$dto->getArrayCopy();
/*
array
  'a' => int 1
  'b' => int 2
  'c' => int 3
  0 => int 4
  1 => int 5
  2 => int 6
*/

Sprawdzanie czy pod danym indeksem kryją się jakieś dane

Do sprawdzenia istnienia pewnych zmiennych standardowo używa się funkcji „isset”. Taką możliwość mamy równierz w tym przypadku.

isset($dto->a);
// boolean true
 
isset($dto["b"]);
// boolean true
 
isset($dto->{0});
// boolean true
 
isset($dto[1]);
// boolean true
 
isset($dto->e);
// boolean false
 
isset($dto[3]);
// boolean false

Należy zwrócić uwagę na działanie funkcji „isset” w sytuacji przypisania do zmiennej wartości NULL. W przypadku standardowych tablic, jeżeli do jakiegoś klucza przpiszemy wartość null to użycie funkcji „isset” zwróci wynik negatywny.

$a = array("foo" => null);
isset($a["foo"]);
// boolean false

Jeśli to samo zrobimy z obiektem ArrayObject otrzymamy wynik zgoła inny. Funkcja „isset” na obiekcie ArrayObject działa tak jak funkcja „array_key_exists” na standardowej tablicy czyli tak naprawdę sprawdza istnienie klucza bez uwzględniania wartości.

$aO = new ArrayObject(array("foo" => null));
isset($aO["foo"]);
// boolean true

Moglibyśmy oczywiście sprawić aby działanie funkcji „isset” na naszym obiekcie DTO było identyczne jak na klasycznej tablicy przedefiniowując metodę offsetExists należącą do interfejsu ArrayObject. Ja jednak zdecydowałem się tego nie zmieniać i dlatego też:

$dto->d = null;
isset($dto->d);
// boolean true
 
$dtoArr = $dto->getArrayCopy();
isset($dtoArr["d"]);
// boolean false

Usuwanie danych z tablicy i ich zliczanie

Jak należało się domyślać usuwanie danych z obiektu DTO przeprowadza się przy pomocy funkcji „unset” z kolei policzyć je można przy pomocy „count”

count($dto);
// int 7

Dla porównania…

count($dto->getArrayCopy());
// int 7

Czas na usunięcie kilku elementów.

unset($dto->c);
unset($dto["d"]);
unset($dto->{0});
unset($dto[1]);
 
count($dto);
// int 3
/*
array
  'a' => int 1
  'b' => int 2
  2 => int 6
*/

Iteracja po elementach tablicy

Dzięki temu, że ArrayObject implementuje interface IteratorAggregate, oparty o niego obiekt DTO można użyć bezpośrednio w pętli foreach

foreach ($dto as $key => $value) {
	$dto[$key] = $value + 10;
}
/*
array
  'a' => int 11
  'b' => int 12
  2 => int 16
*/

Pewnym ograniczeniem jest możliwość usuwania elementów z obiektu w czasie iteracji. Poniższy kod spowoduje wyświetlenie ostrzeżenia „ArrayIterator::next(): Array was modified outside object and internal position is no longer valid …”, ale uzyskanie poprawnego wyniku.

foreach ($dto as $key => $value) {
	if ($value % 2 != 0) {
		unset($dto[$key]);
	}
}
/*
array
  'b' => int 12
  2 => int 16
*/

Objeściem problemu jest iteracja po tablicy i działanie na obiekcie.

foreach ($dto->getArrayCopy() as $key => $value) {
	if ($value % 2 != 0) {
		unset($dto[$key]);
	}
}
/*
array
  'b' => int 12
  2 => int 16
*/

Sortowanie

Zmienne w naszym obiekcie DTO możemy posortować przy użciu kilku metod sortujących („asort”, „ksort”, „natsort”, „natcasesort”, „uasort”, „ukasort”). W tym przypadku nie możemy jednak użyć obiektu jak tablicy gdyż natywne funkcje sortujące nie przyjmą obiektu zamiast tablicy.

$aO = new bigWeb_Dto(array("Zofia", "Joanna", "Krystyna", "Maria"));
$bO = new bigWeb_Dto(array(1, 3, 5, 4, 2));
 
$aO->natsort();
/*
array
  1 => string 'Joanna' (length=6)
  2 => string 'Krystyna' (length=8)
  3 => string 'Maria' (length=5)
  0 => string 'Zofia' (length=5)
*/
 
$bO->natsort();
/*
array
  0 => int 1
  4 => int 2
  1 => int 3
  3 => int 4
  2 => int 5
*/
 
$aO->ksort();
/*
array
  0 => string 'Zofia' (length=5)
  1 => string 'Joanna' (length=6)
  2 => string 'Krystyna' (length=8)
  3 => string 'Maria' (length=5)
*/
 
$bO->ksort();
/*
array
  0 => int 1
  1 => int 3
  2 => int 5
  3 => int 4
  4 => int 2
*/

Istnieje sporo sytuacji, kiedy zwykłe tablice okazują się być niewygodne w użyciu i kiedy trzeba sięgnąć po bardziej wyrafinowane środki. Okazuje się, że aby skorzystać z zalet obiektu DTO nie trzeba od razu projektować systemów rozproszonych, i że można go użyć w wielu sytuacjach – często w połączeniu z innymi wzorcami projektowymi.

O zastosowaniach w kolejnym wpisie 🙂

Posted in PHP by Zbigniew Heintze · Tags: ,