Ajax i CakePHP w wersji 3

Prędzej czy później (raczej prędzej), podczas pisania aplikacji, programista spotka się z sytuacją, w której konieczne będzie dynamiczne pobranie zawartości z bazy danych i podstawienie ich do określonej kontroli na stronie. Jak wiemy CakePHP czy inne frameworki nie umożliwia takiej możliwości, stąd konieczne staje się wykorzystanie Ajaxa. Jak się za chwilę przekonacie, CakePHP i Ajax świetnie ze sobą współpracują i znając kilka podstaw, można rozbudowywać podstawową wiedzę i wykorzystywać tę technikę we własnych aplikacjach.

Jakie jest zastosowanie w praktyce?

Oto kilka przykładowych zastosowań:

  1. Wyobraź sobie sytuację, gdzie masz stronę, na której w polu combo wybierasz osobę, zaś po wybraniu, skrypt ma pobrać jej dane z bazy danych i wyświetlić je w dowolnym miejscu na tej stronie.
  2. Są dwa pola combo: państwo i miejscowość. Po wybraniu państwa, w drugim polu combo chcesz wyświetlić miasta.
  3. Masz pole combo – kategorie oraz drugie – artykuły. Po wybraniu kategorii, należy bez przeładowania strony podstawić listę artykułów z wybranej kategorii.

To tylko kilka zastosowań, zaś inne z pewnością pojawią się podczas tworzenia aplikacji. Jednakże samo zrozumienie techniki Ajax pozwoli na tworzenie o wiele bardziej rozbudowanych zastosowań.

Jako przykład napiszemy prostą aplikację, która przechowuje dane osoby wraz z państwem oraz miastem.

Zakładam, że masz już zainstalowany CakePHP i środowisko pracy. Jeśli nie, przejdź na tę stronę i przejdź przez etap instalacji i konfiguracji.

Przygotowanie:

Utwórz bazę danych mySQL i utwórz tabele wraz z zależnościami:

CREATE TABLE `cities` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `country_id` int(11) UNSIGNED DEFAULT NULL,
  `city` varchar(45) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `countries_fk_idx` (`country_id`),
  CONSTRAINT `countries_fk` FOREIGN KEY (`country_id`) REFERENCES `countries` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
CREATE TABLE `countries` (
 `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
 `country` varchar(45) NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
 
CREATE TABLE `persons` (
 `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
 `name` varchar(40) DEFAULT NULL,
 `surname` varchar(45) DEFAULT NULL,
 `address` varchar(100) DEFAULT NULL,
 `country_id` int(11) UNSIGNED DEFAULT NULL,
 `city_id` int(11) UNSIGNED DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `country_fk_idx` (`country_id`),
 KEY `city_fk_idx` (`city_id`),
 CONSTRAINT `city_fk` FOREIGN KEY (`city_id`) REFERENCES `cities` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
 CONSTRAINT `country_fk` FOREIGN KEY (`country_id`) REFERENCES `countries` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Teraz należy dodać dane przykładowe do tabel countries oraz cities

INSERT INTO `countries` VALUES (1,'Polska'),(2,'Niemcy'),(3,'Ukraina'),(4,'Czechy'),(5,'Słowacja');
INSERT INTO `cities` VALUES (1,1,'Warszawa'),(2,1,'Stalowa Wola'),(3,1,'Rzeszów'),(4,1,'Poznań'),(5,1,'Wrocław'),(6,2,'Berlin'),(7,2,'Hanower'),(8,2,'Monachium'),(9,2,'Kolonia'),(10,3,'Lwów'),(11,3,'Charków'),(12,3,'Kijów'),(13,3,'Donieck'),(14,4,'Praga'),(15,4,'Brno'),(16,4,'Cieszyn'),(17,4,'Ostrawa'),(18,5,'Bratysława'),(19,5,'Korzyce'),(20,5,'Preszów'),(21,5,'Poprad');

Skonfiguruj CakePHP do połączenia z bazą danych

'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'uzytkownik',
            'password' => 'haslo',
            'database' => 'baza',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'flags' => [],
            'cacheMetadata' => true,
            'log' => false,
            'quoteIdentifiers' => false,
            'url' => env('DATABASE_URL', null),
        ],

Jeśli masz pewność, że CakePHP jest prawidłowo połączony z bazą danych, przejdź do katalogu projektu i wygeneruj źródła aplikacji PHP za pomocą konsoli bake:

bin/cake bake all persons
bin/cake bake all countries
bin/cake bake all cities

Jeśli korzystasz z Windows zamień / na \

Możesz już przetestować działanie witryny. Uruchom serwer: bin\cake serve i w przeglądarce wpisz: http://localhost:8765. Można teraz otwierać poszczególne podstrony za pomocą adresu. Np. aby otworzyć stronę osób, wpisz: http://localhost:8765/persons, państw: http://localhost:8765/countries, zaś miast: http://localhost:8765/cities.

Aby domyślną stroną po otwarciu witryny był panel zarządzania osobami, otwórz plik config\routes.php i zmodyfikuj go:

<?php
use Cake\Core\Plugin;
use Cake\Routing\RouteBuilder;
use Cake\Routing\Router;
use Cake\Routing\Route\DashedRoute;
 
Router::defaultRouteClass(DashedRoute::class);
Router::scope('/', function (RouteBuilder $routes) {
    $routes->connect('/', ['controller' => 'Persons', 'action' => 'index']);
    $routes->fallbacks(DashedRoute::class);
});
Plugin::routes();

Od teraz, po wpisaniu adresu http://localhost:8765, będzie się otwierać strona z osobami.

Spróbujmy teraz dodać nową osobę przyciskiem Add person.

Jak widać, mamy tutaj dwa problemy. Po pierwsze, zamiast nazw, pojawiają się numery. Po drugie, przy wyborze państw, należy zawęzić miasta przypisane do tych państw.

Pierwszy problem naprawia się w bardzo prosty sposób. Należy zmodyfikować jeden wierz w modelu Countries oraz Cities, zmieniając $this->setDisplayField(‚id’); na właściwe pola.

Model Countries ma wyglądać tak:

public function initialize(array $config)
    {
        parent::initialize($config);
 
        $this->setTable('countries');
        $this->setDisplayField('country');
        $this->setPrimaryKey('id');
 
        $this->hasMany('Cities', [
            'foreignKey' => 'country_id'
        ]);
        $this->hasMany('Persons', [
            'foreignKey' => 'country_id'
        ]);
    }

Zaś model Cities tak:

public function initialize(array $config)
    {
        parent::initialize($config);
 
        $this->setTable('cities');
        $this->setDisplayField('city');
        $this->setPrimaryKey('id');
 
        $this->belongsTo('Countries', ['foreignKey' => 'country_id']);
        $this->hasMany('Persons', ['foreignKey' => 'city_id']);
    }

Zapisz zmiany i ponownie przetestuj aplikację. Tym razem w polach kombi będą wyświetlane nazwy zamiast ich id.

Drugi problem, którym jest wyświetlanie wyłącznie miast przypisanych do państw nie da się rozwiązać w tak prosty sposób, bowiem należy dynamicznie i bez przeładowania strony podstawiać do pola combo id miast z wybranego zakresu.

Zasada jest taka: w widoku, w którym jest pole combo, za pomocą biblioteki JQuery podłącza się zdarzenie change do procedury, by przy każdej zmianie pola wywołać akcje w kontrolerze, która pobierze dane. Następnie odebrane dane podstawiane są pod kolejne pole combo. Co istotne, kontroler musi zwrócić dane w formacie JSON, bowiem JavaScript bardzo lubi ten format 🙂

Jak to zrobić?

Po pierwsze, należy dodać do głównego szablonu aplikacji odwołanie do biblioteki JQuery. Plik ten to src/Templates/Layout/default.php.

<?= $this->Html->script('https://code.jquery.com/jquery-1.12.4.js') ?>

Następnie przechodzimy do kontrolera, z którego będziemy pobierać dane. W naszym przypadku może być to kontroler CitiesController, jednak może to być inny kontroler, jednak należy pamiętać do dołączeniu modelu: $this->loadModel(‚innymodel’);

Otwórz więc src/Controller/CitiesController.php i dopisz akcję:

public function getcities($id=null) {
     $this->autoRender = false;
     $cities = $this->Cities->find('all',['conditions'=>['country_id'=>$id]]);
     echo json_encode($cities);
   }

Jak widać nowa akcja nie różni się od innych, jednakże po pierwsze, nie wywołuje wdoku ($this->autoRender = false), a po drugie, zwraca dane w formacie JSON: echo json_encode($cities). Jak sprawdzić jej działanie? Wystarczy wpisać w adres przegądarki http://localhost:8765/cities/getcities/1, gdzie numer oznacza id państwa. W przypadku 1 (Polska), w przeglądarce zostaną wyświetlone dane w formacie JSON:

[{"id":1,"country_id":1,"city":"Warszawa"},{"id":2,"country_id":1,"city":"Stalowa Wola"},{"id":3,"country_id":1,"city":"Rzesz\u00f3w"},{"id":4,"country_id":1,"city":"Pozna\u0144"},{"id":5,"country_id":1,"city":"Wroc\u0142aw"}]

Teraz, w pliku widoku src/Templates/Persons/add.ctp dokonujemy istotnych zmian:

Po pierwsze należy dodać parametry id obu pól combo, aby móc się do nich odwołać z JavaScript:

Na początku pliku należy dodać linię:

<?php use Cake\Routing\Router; ?>

echo $this->Form->control('country_id', ['id'=>'country','options' => $countries, 'empty' => true]);
echo $this->Form->control('city_id', ['id'=>'city','options' => $cities, 'empty' => true]);

A następnie dopisać wywołanie zdarzenia change pola kombi countries i napisać jego obsługę (wklej na końcu pliku):

<script>
$('#country').change(function(){
  var idcountry = $(this).val();
   $.ajax({
     dataType: "json",
     type: "POST",
     evalScripts: true,
     async:false,
     url: '<?php echo Router::url(array('controller'=>'cities','action'=>'getcities'));?>' +'/'+idcountry,
      success: function(data){
        $("#city").find('option').remove();
    for (var key in data) {
     if (data.hasOwnProperty(key)) {
      $('#city').append('<option value="'+ data[key]["id"] +'">'+ data[key]["city"]+'</option>');
       } }
      },
       error: function(e) {
        alert("Błąd pobierania miast: " + e.responseText.message);
         }
     });
 });
 </script>

I to wszystko. Wystarczy dokonać podobnych zmian w pliku edit.ctp i gotowe. Oczywiście osobiście preferuję używanie Element, jednak nie chcę komplikować. Zdaję sobie sprawę, że kod da się jeszcze poprawić, jednak celem niniejszego wpisu było jedynie zaprezentowanie metody użycia Ajaxa z frameworkiem CakePHP.

3224total visits,1visits today

Tagi , , .Dodaj do zakładek Link.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

5 + 5 =