Javascript: Przypisać this, czyli metoda apply i call obiektu Function

Czasami, szczególnie przy obiektowym javascriptcie istnieje potrzeba określenia czym jest this w danej funkcji/metodzie. Istnieją dwa sposoby określenia this, za pomocą metod apply i call obiektu Function:

function car() {
    this.name = 'car';
}
car.prototype.getName = function() {
    alert('car.getName(): '+this.name);
}
 
function person() {
    this.name = 'person';
}
person.prototype.getName = function() {
     alert('person.getName(): '+this.name);
}
person.prototype.test1 = function(car) {
     car.getName.apply(this);  
}
person.prototype.test2 = function(car) {
     car.getName.call(this);  
}
 
 
var car = new car();
var person = new person();
car.getName();
person.getName();
person.test1(car);
person.test2(car);

Pierwszym parametrem w obydwu metodach jest zmienna, która w wywołanej funkcji/metodzie będzie przypisana do this. Wykonując powyższy kod, tworzymy 2 obiekty, person i car. Wywołujemy po koleji metody getName obudwu obiektów i dostajemy w komunikatach:

car.getName(): car
person.getName(): person

Czyli wszystko standardowo. W obiekcie car this=car, a w obiekcie person this=person; W kolejnych 2 wywołaniach, metody test1 i test2 obiektu person, wywołujemy metode getName obiektu car, przekazując w pierwszym argumencie metod apply/call this, czyli person(bo wywołujemy z poziomu obiektu person). Spodziewamy się wywołać z poziomu obiektu car metode getName z this=person. Brzmi absurdalnie, ale w kolejnych dwóch komunikatach dostajemy:

car.getName(): person
car.getName(): person

Przykład ten nie niesie większego sensu, ale wydaje mi się, że dobrze oddaje ideę działania tych metod. Metody apply i call różnią się sposobem przekazywania dodatkowych argumentów, co nie zostanie omówione w tym artykule.

CakePHP: HABTM – podstawy relacji hasAndBelongsToMany

1. Wprowadzenie

W artykule tym opiszę wszystkie istotne informacje dotyczące relacji HABTM(hasAndBelongsToMany). Na początku podam przykład takiej relacji wzięty z tej strony. Mamy artykuły i kategorie; artykuł może być przypisany do wielu kategorii, a kategoria może posiadać wiele artykułów. To jest istotą relacji HABTM. Aby utworzyć tą relację konieczne jest stworzenie w bazie danych, tabeli łączącej artykuły z kategoriami. Definicja w kodzie wygląda następująco:

<?php
//definicja modelu artykułów
class Article extends AppModel {
     var $name = 'Article';
     var $hasAndBelongsToMany = array('Category');//definicja relacji HABTM
}
<?php
//definicja modelu kategorii
class Category extends AppModel {
     var $name = 'Category';
     var $hasAndBelongsToMany = array('Article');//definicja relacji HABTM
}

W powyższym kodzie nie zdefiniowałem tabeli łączącej obydwa modele, jest spowodowane tym, że CakePHP domyślnie przyjmuje nazwę wziętą z konwencji; w tym wypadku articles_categories. Dodatkowo zostanie utworzony automodel ArticlesCategory, który umożliwi dostęp bezpośrednio do danych zawartych w tabeli łączącej. Należy jednak pamiętać o tym, że utworzony automodel nie jest powiązany ani z artykułem, ani z kategorią. Powiązanie takie można wywołać ręcznie:

//utworzenie relacji naszego automodelu do kategorii
$this->Article->ArticlesCategory->bindModel(array(‘belongsTo’=>array(‘Category’)));

2. Modyfikacja relacji

Co jednak jeśli nazwa automodelu nam nie pasuje? Albo chcemy użyć innej nazwy tabeli łączącej? Nazwe te możemy dowolnie modyfikować, należy jednak pamiętać o tym aby zmiany wprowadzić w obu modelach. Wprowadzenie ich tylko w jednym modelu będzie skutkowało dużym bałaganem i niespodziewanym zachowaniem aplikacji.

<?php //definicja modelu artykułów class Article extends AppModel { var $name = 'Article'; var $hasAndBelongsToMany = array( 'Category'=>array(
               'with'=>'ArticlesCategories' //nowa nazwa automodelu
               ,'joinTable'=>'articles_categories_joined' //nowa nazwa tabeli łączącej
          )
     );//definicja relacji HABTM
}
<?php //definicja modelu kategorii class Category extends AppModel { var $name = 'Category'; var $hasAndBelongsToMany = array( 'Article'=>array(
               'with'=>'ArticlesCategories' //nowa nazwa automodelu
               ,'joinTable'=>'articles_categories_joined' //nowa nazwa tabeli łączącej
          )
     );//definicja relacji HABTM
}

Po zastosowaniu podanego wyżej kodu nasz automodel będzie się nazywał ArticlesCategories a tabela łącząca articles_categories_joined.

 

3. Analiza relacji

Wrócmy jednak do nazewnictwa proponowanego przez konwencję CakePHP. Jeśli chcemy wejść w szczegóły relacji HABTM, jest to nic innego niż 2 relacje hasMany z zastosowaniem modelu pośredniego. Podaję przykład jak utworzyć relację zachowującą się podobnie jak HABTM:

//definicja modelu artykułów
class Article extends AppModel {
     var $name = 'Article';
     var $hasMany = array('ArticleCategory');
}
 
 
//definicja modelu kategorii
class Category extends AppModel {
     var $name = 'Category';
     var $hasMany = array('ArticleCategory');
}
 
//definicja modelu łączącego
class ArticleCategory extends AppModel {
     var $name = 'ArticleCategory';
     var $belongsTo = array('Article','Category');
}

Jak widać wymaga to zdefiniowania dodatkowego modelu, pełniącego rolę łącznika. Nie polecam definiowania relacji w ten sposób, przykład ten został podany jedynie w celu zrozumienia zasady działania relacji HABTM.

 

4. Tworzenie i usuwanie relacji

Relacja HABTM jest dość specyficzną relacją. Aby przypisać artykuł o id=1 do kategorii o id=1 wywołujemy:

$this->Article->save(array(
    'Article' => array(
        'id' => 1,
    ),
    'Category' => array(
        'Category' => array(1),
    ),
)); 

Jeżeli chcemy przypisać nasz artykuł dodatkowo do kategorii 2 może skorzystać z kodu:

$this->Article->save(array(
    'Article' => array(
        'id' => 1,
    ),
    'Category' => array(
        'Category' => array(2),
    ),
));

Spowoduje to jednak usunięcie relacji z kategoria o id=1. Należy zatem pamietać, że przy modyfikacji relacji HABTM CakePHP najpierw kasuje wszystkie istniejące relacje. Poprawny kod wygląda następująco:

$this->Article->save(array(
    'Article' => array(
        'id' => 1,
    ),
    'Category' => array(
        'Category' => array(1,2),
    ),
));

Jak można się już domyślić, jeśli chcemy aby nasz artykuł nie należał do żadnej kategorii wywołujemy:

$this->Article->save(array(
    'Article' => array(
        'id' => 1,
    ),
    'Category' => array(
        'Category' => array(),
    ),
));

Aby zmienić to zachowanie można skorzystać z add-delete-habtm-behavior. Osobiście nie korzystałem z tego dodatku, ale w niektórych przypadkach może on okazać sie użytyczny.

 

5. Więcej informacji

Cookbook o HABTM

W przyszłości planuję opisać sposób filtrowania danych objętych relacją HABTM.

MySQL: Reorganizacja kluczy głównych tabeli

Krótki post o tym jak należy postępować gdy chcemy dokonać reogranizacji kluczy głównych tabeli. Tyczy się to tylko tabel, które mają indeks główny numerowany automatycznie. Przykładowo mamy tabelę w której istnieją wpisy dla kluczy głównych: 1,3,5,10, a chcemy ją tak przenumerować, żeby te klucze przyjmowały wartości: 1,2,3,4. Rozwiązanie sprowadza się do 3 zapytań SQL:

ALTER TABLE `users` DROP `id`;
ALTER TABLE `users` AUTO_INCREMENT = 1;
ALTER TABLE `users` ADD `id` int UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;

Oczywiście rozwiązanie posiada jedną ogromną wadę: nadaje się do zastosowania tylko tam, gdzie nie mamy relacji z tabelą którą modyfikujemy. Na szczęscie mój obiekt testowy spełniał to założenie.

CakePHP: Optymalizacja tabel ACL

Natknąłem się w sieci na ciekawy kawałek kodu SQL, który drastycznie zwiększa wydajność zapytań do tabel ACL. Zmiana polega jedynie na nałożeniu indeksów i kluczy obcych. Kod został znaleziony na grupie dyskusyjnej CakePHP.

/* ACL Tables */
 
CREATE TABLE acos (
    id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
    parent_id INT DEFAULT NULL,
    model VARCHAR(255) DEFAULT '',
    foreign_key INT UNSIGNED DEFAULT NULL,
    alias VARCHAR(255) DEFAULT '',
    lft INT DEFAULT NULL,
    rght INT DEFAULT NULL
) ENGINE = INNODB;
-- table name is quoted because it is a reserved word
CREATE INDEX idx_acos_lft_rght ON `acos`(lft,rght);
CREATE INDEX idx_acos_alias ON `acos`(alias);
CREATE INDEX idx_acos_model_foreign_key ON `acos`(model(255),foreign_key);
 
CREATE TABLE aros (
    id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
    parent_id INT DEFAULT NULL,
    model VARCHAR(255) DEFAULT '',
    foreign_key INT UNSIGNED DEFAULT NULL,
    alias VARCHAR(255) DEFAULT '',
    lft INT DEFAULT NULL,
    rght INT DEFAULT NULL
) ENGINE = INNODB;
-- table name is quoted because it is a reserved word
CREATE INDEX idx_aros_lft_rght ON `aros`(lft,rght);
CREATE INDEX idx_aros_alias ON `aros`(alias);
CREATE INDEX idx_aros_model_foreign_key ON `aros`(model(255),foreign_key);
 
CREATE TABLE aros_acos (
    id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
    aro_id INT UNSIGNED NOT NULL,
    aco_id INT UNSIGNED NOT NULL,
    _create CHAR(2) NOT NULL DEFAULT 0,
    _read CHAR(2) NOT NULL DEFAULT 0,
    _update CHAR(2) NOT NULL DEFAULT 0,
    _delete CHAR(2) NOT NULL DEFAULT 0
) ENGINE = INNODB;
-- table names are quoted because they are reserved words
CREATE UNIQUE INDEX idx_aros_acos_aro_id_aco_id ON `aros_acos`(aro_id, aco_id);
ALTER TABLE aros_acos ADD CONSTRAINT FOREIGN KEY (aro_id) REFERENCES `aros`(id);
ALTER TABLE aros_acos ADD CONSTRAINT FOREIGN KEY (aco_id) REFERENCES `acos`(id);

CakePHP: Checkbox zamiast multiselect

Załóżmy, że mamy model Post i Tag posiadający relację HABTM. Tworząc widok dodawania nowego postu chcemy mieć możliwość wyboru tagów które są przypisane do postu. Standardowo napiszemy w naszym widoku:

echo $this->Form->input('Tags', array(
    'label'=>__('Tagi',true)
));

Spowoduje to stworzenie elementu select z opcją wielokrotnego wyboru. Jest to element na ogół kłopotliwy ze względu na konieczność używania przycisku control w trakcie zaznaczanie, co ze strony użyteczności jest wielkim nieporozumieniem. CakePHP przychodzi nam z pomocą i umożliwia bardzo proste rozwiązanie problemu. Wystarczy do tablicy parametrów dodać klucz multiple z wartością checkbox. Skutkuje to stworzeniem serii elementów checkbox. Całość wygląda następująco.

echo $this->Form->input('Tags', array(
    'label'=>__('Tagi',true)
    ,'multiple'=>'checkbox'
));

Na ogół jest to rozwiązanie lepsze niż element select. Sposobem najlepszym jest jednak stworzenie elementu ktory nazywam ‚multiselect-checkbox’. Do stworzenia jego potrzebujemy jednak użyć nieco javascriptu. Można się wkrótce spodziewać artykułu na ten temat.

jQuery: Jak sprawdzić czy element istnieje?

Jak sprawdzić czy element istnieje za pomocą jQuery? Jak sprawdzić ile takich elementów istnieje? Ostatnio spotkałem się z takim problemem. Naturalnym odruchem jest napisanie następującego kawałka kodu:

var x = $('.some-element');
if(x) {//zakładamy że ten fragment kodu wykona się tylko wtedy gdy element istnieje
   alert('Błędne rozwiązanie!');
}

Niestety rozwiązanie to jest jak najbardziej błędne. Zmienna x zawsze będzie istnieć, będzie obiektem jQuery. Prawidłowy sposób polega na sprawdzeniu właściwości length tego obiektu. Wartość tej właściwości jes równa ilości znaleźionych elementów.

var x = $('.some-element');
if(x.length) {
   alert('Znaleziono ' + x.length + ' elementów.');
}

Rozwiązanie proste, ale może wpędzić w chwilę zakłopotania, jeżeli nie znamy prawidłowego sposobu.

CakePHP: Korzystanie z helpera z poziomu innego helpera

Co należy zrobić żeby móc korzystać z jakiegoś helpera w CakePHP z poziomu innego helpera? Zadanie jest banalne i nie różni się niczym od korzystania z helperów w zwykłym kontrolerze. Zarówno sposób zaincludowania jak i sposób dostępu do helpera jest identyczny. W zaprezentowanym fragmencie kodu korzystam z HtmlHelper z poziomu stworzonego przezemnie MyHelper.

class MyHelper extends AppHelper {
    var $helpers = array('Html');
 
    function myMethod() {
        return $this->Html->link('Moja strona', 'http://tomaszmazur.eu');
    }
}

CakePHP: Dynamiczne ładowanie zewnętrznych modeli

Czasami zdarza się konieczność korzystania z modelu niepowiązanego z danym modelem/kontrolerem. Przykładem z życia wziętym jest model Setting – prosta tablica zawierajaca różne ustawienia do naszej aplikacji, prawdopodobnie modyfikowalne z poziomu panelu administracyjnego. Będziemy korzystać z tego modelu w bardzo wielu niepowiązanych modelach. W moich aplikacjach model ten zazwyczaj nie posiada żadnych powiązań. Częstym błędem początkujących programistów CakePHP jest próba korzystania z modelu z poziomu kontrolerów za pomocą właściwości $uses.

var $uses = array('Model1','Setting')

Spowoduje to załadowanie modelu Setting przy każdym uruchomieniu kontrolera. Jezeli kontroler posiada metody(akcje) dla których model Setting jest niepotrzebny, to podany kawalek kodu nie jest optymalny. Korzystanie z modelu Setting obywa sie teraz na zasadach analogicznych do korzystania z natywnego modelu kontrolera.

$results = $this->Setting->findById('1');

Zalecaną metodą ładowania zewnętrznych modeli jest ładowanie dynamiczne. Następującą linijkę kodu należy umieścić w metodzie wymagającej dostęp do modelu Setting.

$this->loadModel('Setting');

Korzystanie z modelu odbywa się na zasadach analogicznych do metody z $uses. Jest to zalecana metoda korzystania z zewnętrznych modeli.

 

Jeżeli zajdzie konieczność skorzystania z zewnętrznego, niepowiązanego modelu z poziomu jakiegos modelu, z przyczyn oczywistch nie możemy skorzystać z powyższej metody. Informacja dla nieco mniej obeznanych czytelników, metoda loadModel istnieje w klasie Controller, która jest niedostępna z poziomu modelu. Należy zatem skorzystać z ClassRegistry::init() w następujący sposób

$setting = ClassRegistry::init("Setting");
$results = $setting->findById('1');

Dodam, że nie zdażyła mi się jeszcze koniecznośc załadowania zewnętrznego modelu z poziomu innego modelu. Można oczywiście korzystać z instrukcji ClassRegistry::init() z poziomu kontrolera. Należy jednak pamiętać, aby przypisać model do zmiennej, z której będzie korzystać.

$setting = ClassRegistry::init("Setting");
//$results = $this->Setting->findById('1');
$results = $setting->findById('1');

Zakomentowana linijka nr 2 jest niepoprawna i zwróci błąd. Tyle w temacie dynamicznego ładowania modeli.

CakePHP: IdeaCache – Internal Server Error 500

Artykuł ten jest kontynuacją poprzedniego, dotyczącego błędu w aplikacjach CakePHP na serwerach home.pl. Element, który sprawia kłopot na tych serwerach to IdeaCache. Ten i podobne problemy zostały opisane w następujących linkach:

1. http://cakephp.lighthouseapp.com/projects/42648/tickets/817-class-cakelog-not-found

2. http://cakephp.lighthouseapp.com/projects/42648/tickets/923

Aby naprawić ten błąd należy zamienić kawałek kodu w pliku /cake/libs/cake_log.php

z

if (!defined('DISABLE_DEFAULT_ERROR_HANDLING')) {
    set_error_handler(array('CakeLog', 'handleError'));
}

na

if (!defined('DISABLE_DEFAULT_ERROR_HANDLING')) {
    $cakeLog =& CakeLog::getInstance();
    set_error_handler(array(&$cakeLog, 'handleError'));
}

Musimy pamiętać o tym, że jest to ingerencja w źródła CakePHP. Przy każdej aktualizacji, będziemy musieli podmieniać wyżej wymieniony fragment kodu. Pomimo tej wady, zdecydowałem się na zastosowanie tego fixa. Jeżeli to nie zadziała, to w wyżej wymienionych linkach są również podane inne sposoby naprawy.

CakePHP: home.pl – Internal Server Error 500

UWAGA!

 

Artykuł ten jest nieaktualny! Rozwiązanie problemu jest niepoprawne, prawidłowe rozwiązanie znajduje się w artykule: CakePHP: IdeaCache – Internal Server Error 500

 

Natknąłem się dzisiaj na błąd podczas przenoszenia serwisu opartego na CakePHP na serwer firmy home.pl. Nigdy nie podobała mi się ta firma, ale do tej pory nie stwarzała mi specjalnych kłopotów.

Po wgraniu strony na nowy serwer, ustawilem debug na wartość 2 celem odswieżenia wewnetrznego cache’a CakePHP. Po pomyślnym otworzeniu strony ustawilem debug na wartość 0. Po ponownym otwarciu strony wyskoczył mi błąd Internal Server Error 500. Po przeszukaniu internetu, doszedłem do tego, że należy dodać do pliku .htaccess znajdującego się w nadrzędnym naszej aplikacji, tj. katalog zawierający katalogi app,cake,plugins,vendors; linię RewriteBase /podkatalog/. A zatem cały plik będzie wyglądał następująco

<ifmodule mod_rewrite.c="">
    RewriteEngine on
    RewriteBase /podkatalog/
    RewriteRule    ^$    webroot/    [L]
    RewriteRule    (.*) webroot/$1    [L]
 </ifmodule>

Z moich obserwacji wynika, że bez tej linijki CakePHP nie jest w stanie przeładować swoich wewnetrznych cache. Odpalamy naszą aplikację z debug ustawionym na 2, następnie zmieniamy na 0 i wszystko powinno ładnie chodzić. Co ciekawe mogę również usunąc dodaną linijkę kodu i wszystko będzie działać jak należy. Można wnioskować, że dodanie tej linijki jest konieczne jedynie dlatego, aby CakePHP mógł przeładować wewnętrzne cache. Mam nadzieję, że ktoś z podobnym problemem trafi na ten artykuł.