Ext.Direct i Zend – praktyczne wykorzystanie

Ext.Direct i Zend – praktyczne wykorzystanie

W poprzednim poście omówiliśmy sobie działanie oraz podstawową konfiguracje Ext.Direct’a działającego w koalicji z JSON-owym serwerem opartym na ZendFramework.
W tym poście postaram się przybliżyć działanie mechanizmu na konkretnych przykładach.

Wymagania

Do wykonania i zrozumienie wszystkich zawartych w tym poście ćwiczeń niezbędna będzie podstawowa wiedza na temat:

Model i Store

Ext.Direct swoje główne zastosowanie znajduje w obiektach typu model oraz store, przejdźmy zatem do pobranego wcześniej repozytorium (poprzedni post), a dokładnie do katalogu /public/app/model, a następnie przyglądnijmy się zawartości pliku Player.js

Ext.define('MyApp.model.Player', {
    extend: 'Ext.data.Model',
    fields: [
        { name: 'id', type: 'integer' },
        { name: 'name', type: 'string' },
        { name: 'surname', type: 'string' },
        { name: 'age', type: 'integer' },
        { name: 'nickname', type: 'string' }
    ],
    proxy : {
        // proxy ustawiamy jako direct, co sprawi, że
        // Ext JS sam zadba o odpowiednią komunikacje
        type : 'direct',
        // następnie definiujemy mapowania metod
        // klient->backend, co ważne modele
        // ExtJS również opierają się na CRUD-zie
        api : {
            read : Game.Player.read,
            destroy : Game.Player.destroy,
            create : Game.Player.create,
            update : Game.Player.update
        },
        // nasze dane przesyłane będą w formacie
        // JSON zatem niezbędne jest wskazanie
        // odpowiedniego reader'a
        reader : {
            type : 'json',
            // aby model funkcjonował poprawnie musimy
            // wskazać także pod jakim kluczem znajdować
            // się będą dane
            root: 'Player'
        }
    }
});

Najważniejsze, co zyskujemy dzięki takiej konfiguracji? Po pierwsze statyczną metodę MyApp.model.Player.load, która w błyskawiczny sposób pozwoli nam na pobranie danych.
Zanim ją wypróbujemy musimy zmodyfikować jeden z backend-owych kontrolerów. Otwórz \application\api\Game\Player.php i w metodzie read dodaj warunek jak na poniższym snippet’cie

        // jeżeli w przesłanych parametrach
        // znajduje się konkretne id
        // zwrócona zostanie tylko tablica asocjacyjna
        if(array_key_exists($params, 'id')){
            return array(
                'success' => true,
                'Player' => array(
                    'id' => 0,
                    'name' => 'Sebastian',
                    'surname' => 'Widelak',
                    'age' => 24,
                    'nickname' => 'mkalafior'
                )
            );
        }else{
            return array(
                'success'=>true,
                'Player'=>array(
                    ....
                )
            );
        }

Teraz jesteśmy gotowi, aby przetestować mechanizm w praktyce. Moglibyśmy wymagany kod dopisać do któregoś z frondend-owych kontrolerów, ale równie dobrze możemy go przetestować w konsoli naszej przeglądarki (Chrome – F12), gdzie wystarczy wkleić

MyApp.model.Player.load(1, {
    success: function (record) {
        console.log(record);
    }
});

i wcisnąć enter.
Po pewnej chwili powinniśmy otrzymać oczekiwane wyniki. Zwrócony model zawiera już dane pobrane z serwera.

direct

Ok, następną funkcjonalnością którą przetestujemy będzie tworzenie i zapisywanie danych.
Ponownie, zanim wypróbujemy możliwości Direct’a, potrzebujemy chwili na dołożenie pewnej funkcjonalności na backend’zie.
Zmodyfikuj ponownie uprzednio otwarty php-owski kontroler dodając na koniec pliku trzy poniższe funkcje, które posłużą nam jako adaptery do zapisu danych w sesji.


    /**
     * @param $name
     * @param array $data
     * @return array
     */
    private function _create($name, array $data)
    {
        $namespace = new Zend_Session_Namespace('storage');
        isset($namespace->$name) || $namespace->$name = array();
        $data['id'] = count($namespace->$name);
        array_push($namespace->$name, $data);
        return $data;
    }

    /**
     * @param $name
     * @param $id
     * @param array $data
     * @return array
     */
    private function _update($name, $id, array $data){
        $return = array();
        $namespace = new Zend_Session_Namespace('storage');
        if (is_array($namespace->$name)) {
            foreach ($namespace->$name as $key => &$row) {
                if ($key === (int)$id) {
                    $data['id'] = $id;
                    $row = $data;
                    $return = $data;
                }
            }
        }
        return $return;
    }

    /**
     * @param $name
     * @param $id
     * @return array
     */
    private function _load($name, $id = -1)
    {
        $return = array();
        $namespace = new Zend_Session_Namespace('storage');
        if (is_array($namespace->$name)) {
            if ($id > -1) {
                foreach ($namespace->$name as $key => $row) {
                    if ($key === (int)$id) {
                        $return = $row;
                    }
                }
            } else {
                $return = $namespace->$name;
            }
        }
        return $return;
    }
Funkcje prywatne nie są widoczne dla Ext.Direct’a

Zmodyfikujemy także CRUD-owskie funkcje read, create i update, tak aby wykorzystywały nowe metody.

    /**
     * @param array $params
     * @return array
     */
    public function read($params)
    {
        if ($params['id']) {
            return array(
                'success' => true,
                'Player' => $this->_load('Player', $params['id'])
            );
        } else {
            return array(
                'success' => true,
                'Player' => $this->_load('Player')
            );
        }
    }

    /**
     * @param array $params
     * @return array
     */
    public function create($params)
    {
        return array(
            'success' => true,
            'Player' => $this->_create('Player', $params)
        );
    }

    /**
     * @param array $params
     * @return array
     */
    public function update($params)
    {
        return array(
            'success' => true,
            'Player' => $this->_update('Player', $params['id'], $params)
        );
    }

Nic już nie stoi na przeszkodzie, aby wypróbować wprowadzone zmiany. Najpierw odświeżmy naszą aplikację, grid po lewej powinien być pusty. Otwórzmy konsolę i odpalmy poniższy kod

var player = MyApp.model.Player.create();
player.set({
    name : 'Sebastian',
    surname : 'Widelak',
    age : 24,
    nickname : 'mkalafior'
});
player.save();

Przejdźmy do okna przeglądarki i po raz kolejny wciśnijmy F5. Tym razem w tabeli powinien wyświetlić się dodany rekord.
Ciągłe przeładowywanie całej zawartości strony nie należy do najprzyjemniejszych czynności. Aby przyśpieszyć cały proces odświeżania grida dołożymy więc odpowiedni komponent, który umożliwi nam wyłącznie przeładowanie store’a. Zanim to jednak zrobimy przyjrzyjmy się jego definicji:

Ext.define('MyApp.store.Players', {
    extend : 'Ext.data.Store',
    requires : [
        'MyApp.model.Player'
    ],
    model : 'MyApp.model.Player',
    storeId : "Players",
    autoLoad : true,
    autoSync: true,
    proxy : {
        type : 'direct',
        api : {
            read : Game.Player.read,
            destroy : Game.Player.destroy,
            create : Game.Player.create,
            update : Game.Player.update
        },
        reader : {
            type : 'json',
            root: 'Player'
        }
    }
});

Jak widzimy konfiguracja pól odpowiadająca za proxy niczym nie różni się od tej z modelu.
Dodatkowo pola autoLoad (automatyczne ładowanie store’a po załadowaniu aplikacji) i autoSync (synchronizacja z bazą przy każdej zmianie) ustawione na true pozwolą nam poczuć pełną moc Direct’a.
Przejdźmy do pliku \public\app\view\Viewport.js, znajdźmy linijkę 22 i wymieńmy zawartość tabeli na poniższą

       items : [// linijka 22
            {
                xtype : 'grid',
                border : false,
                columns : [
                    {
                        dataIndex : 'name',
                        flex: 1
                    },
                    {
                        dataIndex : 'surname',
                        flex: 1
                    }
                ],
                dockedItems: [
                    {
                        xtype: 'pagingtoolbar',
                        dock: 'bottom',
                        store : 'Players',
                        displayInfo: true
                    }
                ],
                store : 'Players'
            }
        ],

ostatecznie ustalmy jeszcze odpowiednią szerokość grida tak aby plugin od stronicowania był wyświetlany w całości

        ],// linijka 46
        width: 350 // wartość do zmiany z 150 na min 350

Teraz, aby sprawdzić wynik dodania nowego rekordu do bazy, wystarczy, poprzez kliknięcie button w dolnym toolbarze, odświeżyć sam obiekt i powiązany z nim widok.
Istnieje jeszcze jeden szybki sposób utworzenia nowego wpisu do db, bezpośrednio poprzez store, wystarczy posłużyć się poniższym kodem w konsoli:

var store = Ext.StoreManager.lookup('Players');
store.add({
   name : 'Name',
   surname : 'Surname',
   age : 20,
   nickname: 'nick'
});

Formularze

Kolejnym miejscem gdzie z powodzeniem możemy korzystać z Direct’a są formularze. Przyporządkowując im odpowiednie metody w szybki i wygodny sposób możemy zapewnić sobie możliwość uzupełnienia pól danymi oraz zapis ich wartości do bazy bez jakichkolwiek dodatkowych czynności.
Otwórzmy plik \public\app\view\Viewport.js i znajdźmy poniższy fragment kodu:

{
    region: 'center',
    xtype: 'tabpanel',
    layout: 'fit',
    items: [
        {
            title: 'Details',
            layout: 'fit',
            items: [
                {
                    xtype: 'form',
                    model: 'Player',
                    // dodajemy linijke która wskaże
                    // formularzowi który z wysyłanych
                    // parametrów powinien być wysłany
                    // jako pierwszy
                    paramOrder: ['id'],
                    padding: 5,
                    border: false,
                    // dodajemy konfiguracje
                    // directa
                    api: {
                        // dopiszmy dwie nowe metody służące tylko do obsługi formularza
                        load: 'Game.Player.load',
                        submit: 'Game.Player.submit'
                    },
                    items: [
                        {
                            xtype: 'hiddenfield',
                            name: 'id'
                        },
                        {
                            xtype: 'textfield',
                            name: 'name'
                        },
                        {
                            xtype: 'textfield',
                            name: 'surname'
                        },
                        {
                            xtype: 'textfield',
                            name: 'nickname'
                        },
                        {
                            xtype: 'numberfield',
                            name: 'age'
                        }
                    ],
                    // niezbędne będą też buttony które będą służyć
                    // do wyczyszczenia i wysłania formularza
                    buttons: [
                        {
                            text: 'Reset Form',
                            handler: function (btn) {
                                var form = btn.up('form').getForm();
                                form.reset();
                            }
                        },
                        {
                            text: 'Submit Form',
                            handler: function (btn) {
                                var form = btn.up('form').getForm(),
                                    idField = form.findField('id'),
                                    value = idField ? idField.getValue() : null;
                                form.submit({
                                    params: {
                                        id: value
                                    }
                                });
                            }
                        }
                    ]
                }
            ]
        }
    ]
}

Teraz zadbajmy także aby wspomniane wyżej funkcje znalazły się w naszych backend-owych kontrolerach:


    /**
     * @param $id
     * @return array
     */
    public function load($id)
    {
        return array(
            'success' => true,
            'data' => $this->_load('Player', $id)
        );
    }

    /**
     * @param $id
     * @param $name
     * @param $surname
     * @param $nickname
     * @param $age
     * // w przypadku metody submit
     * // musimy poinformować Direct'a
     * // że metoda ta służy do obsługi
     * // formularza, za to zadanie
     * // odpowiedzialna jest poniższa
     * // adnotacja
     * @formHandler
     * @return array
     */
    public function submit(
        $id,
        $name,
        $surname,
        $nickname,
        $age
    )
    {
        if(ctype_digit($id) || is_int($id)){
            $return = $this->_update('Player', (int)$id, array(
                'name' => $name,
                'surname' => $surname,
                'age' => $age,
                'nickname' => $nickname,
            ));
        }else{
            $return = $this->_create('Player', array(
                'name' => $name,
                'surname' => $surname,
                'age' => $age,
                'nickname' => $nickname,
            ));
        }

        return array(
            'success' => true,
            'data' => $return
        );
    }

Ostatecznie musimy jeszcze minimalnie zmodyfikować linijkę 15 w frondend-owym kontrolerze (\public\app\controller\Main.js):

               'itemclick' : function (grid, record, el, index){
                    var form = Ext.ComponentQuery.query('form')[0]; //linijka 14
                    form.getForm().load({
                        params : {
                            id : record.get('id')
                        }
                    });
                }

Przeładujmy teraz naszą aplikację. Od tego momentu, gdy wyślemy i uzupełnimy pusty formularz, powinniśmy otrzymać nowy rekord. Natomiast kliknięcie na wiersz w gridzie, spowoduje ponowne załadowanie danych w odpowiednie pola (kliknięcie “Submit …” uaktualni rekord).

Wywoływanie bezpośrednie

Ostatnim sposobem użycia Direct’a jest wywołanie zdefiniowanych w backend-owych kontrolerach metod bezpośrednio z dowolnej części aplikacji.
Załóżmy więc, że chcemy zablokować dodawanie nowych osób, o nicku który istnieje już w naszej bazie. Zaczynamy ponownie od backend-u (\application\api\Game\Player.php) dodając metodę, która zwróci true jeżeli nick będzie unikalny i false w przeciwnym przypadku.

    /**
     * @param $nick
     * @return array
     */
    public function isNickUnique($nick)
    {
        $return = array('success' => true);
        $namespace = new Zend_Session_Namespace('storage');
        if (is_array($namespace->Player)) {
            foreach ($namespace->Player as $key => $row) {
                if ($row['nickname'] === $nick) {
                    $return = array('success' => false);
                }
            }
        }
        return $return;
    }

Drobne zmiany obejmą również nasz główny frontend-owy kontroler (\public\app\controller\Main.js). Usuwamy całą zawartość listener’a itemclick i dodajemy poniższą funkcję.

function (grid, record, el, index){
    var form = Ext.ComponentQuery.query('form')[0],
        basic = form.getForm();
    // dodajemy małe zabezpieczenie, aby upewnić się, że
    // formularz po zaczytaniu danych nie wywoła nie potrzebnie
    // walidacji (skoro rekord jest w bazie to znaczy, że jest poprawny)
    basic.getFields().each(function(item, index, length){
        item.suspendCheckChange++;
    });
    basic.load({
        params : {
            id : record.get('id')
        },
        success : function (form, object) {
            // zachowujemy dane pobrane przez load
            // potrzebne nam będą w późniejszej walidacji
            basic.rawData = object.result.data;
            // odwieszamy walidację
            basic.getFields().each(function(item, index, length){
                item.suspendCheckChange--;
            });
        }
    });
}

Następnie otwieramy \public\app\view\Viewport.js i do pola nickname dodajemy walidator

{
    xtype: 'textfield',
    name: 'nickname',
    validator: function (value) {
        var me = this,
            form = me.up('form'),
            basic = form.getForm();
        if (
            me.originalValue == value ||
                me.newValue == value ||
                (basic.rawData && basic.rawData[me.name] == value)
            )
            return true;
        // Puentą całego zadania jest wywołanie metody Direct'a
        // w sposób przedstawiony poniżej.
        // Jak widzimy w pierwszej kolejności podajemy argumenty
        // funkcji (value to wartość dla zmiennej $nick po stronie
        // backendu), a następnie callback czyli to co ma się wykonać
        // gdy otrzymamy odpowiedź z serwera
        Game.Player.isNickUnique(value, function (response) {
            if (response.success == true) {
                me.clearInvalid();
                me.newValue = value;
            }
            else
                return 'Sorry, nickname exists.';
        });
        return 'Sorry, nickname exists.';
    }
}

I to już wszystko, jeżeli chodzi o zastosowanie mechanizmu Ext.Direct. Jeżeli macie jakieś uwagi, pytania, zapraszam do dyskusji w komentarzach.

Komentarze:
piku235
Odpowiedz
08/10/2019 w 9:04 PM

PETER TU BYL!

Zostaw odpowiedź

Proszę wypełnij wymagane pola aby dodać komentarz.


Drag circle to the rectangle on the right!

Wyślij