ES6 – default + rest + spread

javascript

Biorąc pod uwagę całe lata narzekań, ECMAScript 2015 przynosi nam wiele nowości, czego rezultatem jest spora ilość usprawnień. Sprawiają one, że pisanie kodu w JavaScript jest bardziej intuicyjne oraz po prostu szybsze. Spójrzmy zatem na nowy sposób przekazywania parametrów do funkcji.


Zobacz całą serię: Let-s talk about ECMAScript 2015


default

Jest to małe, ale bardzo przydatne usprawnienie w przekazywaniu parametrów do funkcji. Wiemy przecież, że funkcje w języku JavaScript pozwalają na przekazanie do nich dowolnej ilości parametrów. Z pewnością nie raz spotkaliście się z podobnym kodem:

function inc(number, increment) {
  // set default to 1 if increment not passed
  // (or passed as undefined)
  increment = increment || 1;
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

Operator logiczny OR (||) przypisuje w tym wypadku wartość 1 do zmiennej increment, ponieważ lewy człon wyrażenia to undefined, czyli false.

ES6 pozwoli pozbyć się tego typu wyrażeń, wykorzystując w tym celu parametry z domyślnymi wartościami. Funkcja inc z wykorzystaniem składni ES6 prezentuje się zatem następująco:

function inc(number, increment = 1) {
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

Tym samym parametry z wartościami domyślnymi są opcjonalne.

Możemy również definiować domyślne wartości parametrów, które występują w środku sygnatury funkcji:

function sum(a, b = 2, c) {
  return a + b + c;
}
console.log(sum(1, 5, 10));         // 16 -> b === 5
console.log(sum(1, undefined, 10)); // 13 -> b as default

Wykorzystanie wartości domyślnej następuje w tym wypadku w momencie, gdy jako wartość parametru podamy undefined.

Wartości domyślne nie muszą być typami prymitywnymi. Jako domyślną wartość możemy przypisać choćby rezultat wykonania funkcji:

function getDefaultIncrement() {
  return 1;
}
function inc(number, increment = getDefaultIncrement()) {
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

rest

Spróbujmy napisać wcześniejszą funkcję sum tak, by brała pod uwagę wszystkie parametry jakie do niej przekażemy. By zachować czystość kodu, pominiemy tutaj walidację. Jeżeli próbowalibyśmy użyć do tego zadania składni dobrze znanej z ES5, to z pewnością otrzymalibyśmy coś takiego:

function sum() {
   var numbers = Array.prototype.slice.call(arguments),
       result = 0;
   numbers.forEach(function (number) {
       result += number;
   });
   return result;
}
console.log(sum(1));             // 1
console.log(sum(1, 2, 3, 4, 5)); // 15

Przy takim podejściu nie wiemy od razu co robi dana funkcja. Nie wystarczy, że spojrzymy na jej sygnaturę. Musimy jeszcze przeskanować jej ciało, znaleźć obiekt arguments i wywnioskować, że chodzi tutaj o zsumowanie wszystkich parametrów przekazanych do funkcji.

Składnia ES6 znacznie lepiej radzi sobie z podobnym problemem . Wprowadza ona pojęcie rest parameters, gdzie parametr poprzedzony (trzema kropkami) staje się tablicą, do której “wpadają” wszystkie pozostałe parametry przekazane do funkcji.

Obiekt arguments zawierać będzie wszystkie parametry funkcji - te nazwane i nie.

Możemy użyć nowej składni ECMAScript 2015 i przepisać funkcję sum:

function sum(…numbers) {
  var result = 0;
  numbers.forEach(function (number) {
    result += number;
  });
  return result;
}
console.log(sum(1)); // 1
console.log(sum(1, 2, 3, 4, 5)); // 15

Jest jednak jedno ALE: nie możemy posiadać więcej niż jednego parametru typu …rest. W przeciwnym razie otrzymamy błąd jak niżej:

function sum(…numbers, last) { // causes a syntax error
  var result = 0;
  numbers.forEach(function (number) {
    result += number;
  });
  return result;
}

spread

Kolejne wyrażenie, to spread, które przypomina trochę konstrukcję rest ze względu na swoją notację z trzema kropkami. Dzieli tablicę na poszczególne wartości, które przekazywane są jako osobne parametry do ciała funkcji.

Po raz kolejny zdefiniujmy funkcję sum, do której przekażemy wyrażenie typu spread:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum(…args)); // 6

Odpowiednik takiego kodu w ES5, to:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum.apply(undefined, args)); // 6

Zamiast więc używać funkcji apply możemy od teraz posługiwać się “rozbiciem” tablicy na osobne parametry. Rozwiązanie prostsze i według mnie również bardziej czytelne.

Ciekawostką jest fakt, że wyrażenie spread możemy łączyć ze standardowym sposobem przekazywania parametru do funkcji:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2];
console.log(sum(…args, 3)); // 6

Rezultat jest dokładnie taki sam jak w poprzednim przykładzie. Tym razem tablica z dwiema wartościami zostaje rozbita na dwa parametry odpowiadające ab, zaś trzecim parametrem jest liczba 3, która odpowiada zmiennej c.

Autoryzacja API – HMAC PHP

Autoryzacja API

Autoryzacja REST API jest czymś nieco innym niżeli autoryzacja użytkownika na stronie internetowej. Mechanizm  HTTP Basic Auth także tutaj nie pasuje. W przypadku naszego API nie tworzy się żadna sesja, która pozwalałaby na identyfikację konkretnego użytkownika / klienta korzystającego z dostępnych metod. Wykorzystujemy przecież protokół bezstanowy REST.

W minionym tygodniu zmierzyłem się z podobnym problemem w przypadku API, które piszę na potrzeby aplikacji mobilnej. Rozważane były tak naprawdę dwa rozwiązania: OAuth oraz wykorzystanie kluczy publicznych i prywatnych. Otwarty protokół OAuth wydawałby się idealnym rozwiązaniem. Okazuje się jednak, że jest kilka ALE, które komplikują jego implementację:

  • brak standaryzacji – Facebook i Twitter implementują w swoich API OAuth, ale jedno i drugie rozwiązanie różni się od siebie
  • skomplikowana implementacja po stronie serwera oraz aplikacji
  • spory narzut wydajnościowy

Finalne rozwiązanie oparte zostało o zasadę kluczy publicznych i prywatnych, ale zostało ono dostosowane do wymogów aplikacji mobilnej oraz możliwości jakie daje wewnętrzny framework. Chciałbym Wam dzisiaj jednak pokazać rozwiązanie oparte o metodę HMAC, która została zaimplementowana w języku PHP (>= 5.1.2) pod postacią funkcji hash_hmac.

Działanie HMAC

  1. stworzenie tak zwanego content hash przy użyciu private key, który znany jest tylko użytkownikowi (oraz systemowi)
  2. content hash przesyłany jest na serwer razem z public key
  3. serwer odczytuje public key i na jego podstawie szuka użytkownika, który pasuje do owego klucza , a następnie pobiera jego private key
  4. serwer używa pobranego private key do wykonania funkcji hash_hmac, która hashuje treść przesłaną w żądaniu HTTP i porównuje ją z content hash

Klucz publiczny i prywatny

By wygenerować public key i private key można posłużyć się kilkoma metodami. Poniżej pokażę dwie, chyba najprostsze, a zarazem skuteczne sposoby.

  1. Wykorzystanie OpenSSL
    $hash = hash('sha256', openssl_random_pseudo_bytes(32));

    OpenSSL nie jest domyślnym rozszerzeniem w PHP, dlatego też poniższa metoda powinna zastąpić wykorzystanie tej biblioteki.

  2. Wykorzystanie funkcji mt_rand:
    $hash = hash('sha256', mt_rand());

Oba przypadki jako funkcję hashującą wykosztują sha256, której obce są kolizje znane chociażby z funkcji skrótu jaką jest MD5.

Przykład wykorzystania HMAC w PHP

Czas na zabawę, czyli przejście do praktyki. Poniżej kod klienta i serwera, który może posłużyć jako przykład REST API.

Przykład wykorzystuje mechanizm cURL, który dostępny jest raczej w większości instalacji PHP. Zawsze można jednak użyć bardziej niskopoziomowej implementacji z socketami.

Kod klienta

<?php
	$publicKey  = '3441df0babc2a2dda551d7cd39fb235bc4e09cd1e4556bf261bb49188f548348';
	$privateKey = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
	$content    = json_encode(array(
		'test' => 'content'
	));

	$hash = hash_hmac('sha256', $content, $privateKey);

	$headers = array(
		'X-Public: '.$publicKey,
		'X-Hash: '.$hash
	);

	$ch = curl_init('http://test.localhost:8080/api-test/');
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $content);

	$result = curl_exec($ch);
	curl_close($ch);

	echo "RESULT\\n======\\n".print_r($result, true)."\\n\\n";
?>

Kod serwera

Po stronie serwera wykorzystam Slim Framework, który jest tak naprawdę mikroframeworkiem. Idealnie nadaje się on do małych aplikacji, które nie wymagają super mocy. Instalujemy Slim poprzez dodanie do swojego composer.json poniższych linijek:

{
    "require": {
        "slim/slim":"2.*"
    }
}

Composer zainstaluje nam cały framework w katalogu vendor/.

Więcej o Composer znajdziesz na: getcomposer.org

Nasz klient przesyłał do serwera nagłówki X-Public oraz X-Hash, które musimy pobrać po stronie serwera. Private key jest oczywiście hard-coded bezpośrednio w kodzie. W rzeczywistym systemie należałoby odpytać bazę danych i pobrać taką wartość z obiektu użytkownika.

<?php
require_once 'vendor/autoload.php';

$app = new \\Slim\\Slim();
$app->post('/', function() use ($app) {

    $request = $app->request();

    $publicHash  = $request->headers('X-Public');
    $contentHash = $request->headers('X-Hash');
    $privateKey  = 'e249c439ed7697df2a4b045d97d4b9b7e1854c3ff8dd668c779013653913572e';
    $content     = $request->getBody();

    $hash = hash_hmac('sha256', $content, $privateKey);

    if ($hash == $contentHash){
        echo "match!\n";
    }
});
?>

PS. Staram się by wpis miał przyjemną formę i był łatwy do czytania. Udało mi się? Okaże się tym samym kto dotarł do końca.

Notyfikacje PUSH dla urządzeń z Androidem w PHP – GCM

Google Cloud Messaging for Android

Google Cloud Messaging to usługa uruchomiona przez Google niecały miesiąc temu (wcześniej C2DM). Pozwala ona na wysyłanie powiadomień PUSH na urządzenia z Androidem bezpośrednio z serwera obsługującego aplikację mobilną. Takie rozwiązanie posiadają wszystkie największe platformy – iOS, Windows Phone 7, Blackberry, a sama platforma Android praktycznie od samego początku swojego istnienia. Jednak to urządzenia od Apple cieszą się większą popularnością takiej usługi. Google postanowiło to zmienić i zbudowało od nowa całą usługę, tak by ułatwić deweloperom implementację w swoich projektach, a także obsługę takich notyfikacji po stronie serwera aplikacji. Jako, że w pracy przymierzam się do zadania stworzenia ogólnodostępnego API REST na wiele platform mobilnych, postanowiłem, że zacznę od napisania prostej klasy w PHP, która obsługuje właśnie GCM do Google.

Cała dokumentacja usługi od Google dostępna jest na stronie oficjalnej i nie ma sensu tutaj przytaczać tego co bardzo dobrze opisano właśnie tam. Bibliotekę JAR dla Javy mamy dostępną “od ręki” i możemy wykorzystać ją w swoim projekcie, ale z PHP nie jest już tak wesoło i sami musimy podjąć się implementacji GCM po swojej stronie.

Do napisania poniżej klasy posłużyłem się biblioteką Buzz, która jest niczym więcej jak oprogramowanym cURL’em.

<?php

namespace Service\Gcm;

use Buzz\Browser;
use Buzz\Client\MultiCurl;

class Gcm
{
    /**
     * @var string
     */
    protected $apiUrl = 'https://android.googleapis.com/gcm/send';

    /**
     * @var string
     */
    protected $apiKey;

    /**
     * @var string
     */
    protected $registrationIdMaxCount = 1000;

    /**
     * @var \Buzz\Browser
     */
    protected $browser;

    /**
     * @var array
     */
    protected $responses;

    /**
     * Class constructor
     *
     * @param $apiKey
     * @param null $baseUrl
     */
    public function __construct($apiKey, $apiUrl = null)
    {
        $this->apiKey = $apiKey;

        if ($apiUrl) {
            $this->apiUrl = $apiUrl;
        }

        $this->browser = new Browser(new MultiCurl());
    }

    /**
     * Sends the data to the given registration ID's via the GCM server
     *
     * @param mixed $data
     * @param array $registrationIds
     * @param array $options to add along with message, such as collapse_key, time_to_live, delay_while_idle
     * @return bool
     */
    public function send($data, array $registrationIds, array $options = array())
    {
        $headers = array(
            'Authorization: key='.$this->apiKey,
            'Content-Type: application/json'
        );

        $data = array_merge($options, array(
            'data' => $data,
        ));

        // Chunk number of registration ID's according to the maximum allowed by GCM
        $chunks = array_chunk($registrationIds, $this->registrationIdMaxCount);

        // Perform the calls (in parallel)
        $this->responses = array();
        foreach ($chunks as $registrationIds) {
            $data['registration_ids'] = $registrationIds;
            $this->responses[] = $this->browser->post($this->apiUrl, $headers, json_encode($data));
        }
        $this->browser->getClient()->flush();

        // Determine success
        foreach ($this->responses as $response) {
            $message = json_decode($response->getContent());
            if ($message === null || $message->success == 0 || $message->failure > 0) {
                return false;
            }
        }

        return true;
    }

    /**
     * @return array
     */
    public function getResponses()
    {
        return $this->responses;
    }
}

Prawda, że nie wygląda to skomplikowanie? Jest to tak naprawdę klasa-interfejs, do użycia w naszym kodzie, gdzie z bazy danych będziemy mogli pobrać zapisanie urządzenia oraz wysłać im przygotowane powiadomienia. Brakuje tutaj obsługi błędów, ale to dopiero przede mną :)