CakePHP 3 – jak stworzyć bloga. Część 1.

Na początku zaznaczę, że niniejszy opis dotyczy frameworka CakePHP w wersji 3 i wyższej, ponieważ na temat wersji 1 czy 2 wersji powstało wiele tutoriali. Wersja 3 wydaje się być zbyt uboga w przykłady. Niniejszy opis dotyczy stworzenia prostego bloga. W tej części zajmiemy się prostym szkieletem aplikacji. Dodatkowo zostanie opisana struktura projektu oraz najważniejsze pliki. W dalszych częściach zajmiemy się szablonem głównym, logowaniem oraz tworzeniem dynamicznego menu.

Aby przystąpić do realizacji poniższych punktów, należy zainstalować środowisko PHP, Composer, MySQL oraz dowolny edytor, np. Atom, Notepad++, etc.

Na początek założenia do nowego projektu:

Może istnieć wiele kategorii i wiele postów, ale pojedynczy post może być przypisany wyłącznie do jednej kategorii. Na początek utworzymy dwie tabele połączone relacją 1:N (jeden do wielu) – categories oraz articles.

Tabela categories będzie przechowywała kategorie, zaś do każdej z nich, będzie mogło być przypisane wiele artykułów. Struktura tabeli wygląda następująco:

CREATE TABLE blog.categories (
  id int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  name varchar(100) NOT NULL,
  PRIMARY KEY (id)
)

Tabela articles zawierać będzie artykuły (wpisy bloga), z których każdy mógł zostać przypisany do jednej kategorii:

CREATE TABLE blog.articles (
  id int(11) NOT NULL AUTO_INCREMENT,
  title varchar(100) NOT NULL,
  created datetime DEFAULT NULL,
  modified datetime DEFAULT NULL,
  body text NOT NULL,
  category_id int(11) UNSIGNED,
  counter int(11) UNSIGNED DEFAULT 0,
  PRIMARY KEY (id),
  UNIQUE INDEX id_UNIQUE (id),
  CONSTRAINT FK_pages_categories_id FOREIGN KEY (category_id)
  REFERENCES categories (id)
)

W tabeli zauważyć można klucz obcy category_id, który odwołuje się do tabeli categories. Zdefiniowanie klucza pozwala wykluczyć sytuację, w której podczas dodawania lub edycji artykułu będzie można podać dowolne category_id. A takim przypadku system bazodanowy wywoła błąd. Podany category_id musi istnieć w tabeli categories! Jest to bardzo istotne zabezpieczenie chroniące bazę przed utratą spójności. Opcjonalne, a zarazem zalecane jest dodanie REFERENCES categories (id) ON DELETE RESTRICT ON UPDATE RESTRICT, by nie dało się usunąć kategorii, jeśli została ona użyta w którymkolwiek artykule, lecz w niniejszym opisie, który dotyczy konkretnego problemu, pominę ten problem.

Jako ciekawostkę dodam, że pola created i modified (oba typu datetime i domyślnie null), to pola utworzenia i modyfikacji rekordu, które CakePHP wypełnia automatycznie, jeśli zachowana będzie konwencja nazw i typów (jak na przykładzie).

Po utworzeniu bazy danych, należy utworzyć nowy projekt i skonfigurować CakePHP do połączenia z bazą danych.W linii poleceń, będąc w katalogu głównym www, wydajemy polecenie:

composer create-project --prefer-dist cakephp/app blog

gdzie słowo blog oznacza po prostu nazwę nowego projektu.

Instalacja potrwa chwilę, zaś gdy się zakończy, należy skonfigurować połączenie z bazą. Jeśli instalator zapyta o ustawienie zabezpieczeń (Set Folder Permissions ? (Default to Y) [Y,n]?), można wcisnąć Enter.

Teraz należy przejść do katalogu projektu komendą cd blog i uruchomić wewnętrzny serwer www (jeśli nie posiadasz zainstalowanego serwera Apache czy IIS) komendą bin\cake server. Strona będzie dostępna pod adresem: http://localhost:8765 i wygląda tak:

W sekcji Database wyświetlony jest stan połączenia z bazą. Oczywiście domyślnie połączenie jest niedostępne, stąd należy poprawić konfigurację. W tym celu należy otworzyć plik app.php z katalogu config i zmodyfikować nieco jego zawartość. Interesuje nas sekcja „default„, gdzie należy zmienić pogrubione wartości (kod poniżej).

'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'serwer MySql',
            'username' => 'login_do_bazy',
            'password' => 'haslo_do_bazy',
            'database' => 'nazwa_bazy',
            'encoding' => 'utf8',
            'timezone' => '',
            'flags' => [],
            'cacheMetadata' => true,
            'log' => false,
            'quoteIdentifiers' => false,
            'url' => env('DATABASE_URL', null),
        ],

Po zapisaniu pliku, stan połączenia z bazą można sprawdzić ponownie odświeżając stronę. Poprawność połączenia skutkuje wyświetleniem komunikatu CakePHP is able to connect to the database. Jeżeli połączenie z bazą danych jest prawidłowe, można przystąpić do dalszej pracy.

Zakładam, że Jeśli wszystko jest gotowe, można wygenerować modele, kontrolery i widoki (scafolding). W tym celu, będąc w katalogu www w której znajduje się CakePHP, należy posłużyć się komendą bake:

bin\cake bake all categories

bin\cake bake all articles

Komenda bake all łączy się z bazą danych, rozpoznaje typy pól tabeli oraz powiązania pomiędzy tabelami na podstawie konwencji nazw i buduje Modele, Controlery oraz Widoki.

Wskazówka: jeżeli nie pamiętasz, jakie tabele posiadasz w bazie danych, wpisz polecenie:

bin\cake bake all

Spowoduje to wyświetlenie dostępnych tabel:

Welcome to CakePHP v3.4.5 Console
---------------------------------------------------------------
App : src
Path: C:\www\blog\src\
PHP : 7.0.9
---------------------------------------------------------------
Bake All
---------------------------------------------------------------
Possible model names based on your database:
- articles
- categories
Run `cake bake all [name]` to generate skeleton files.

Jak widać, mamy dostępne dwie tabele: articles oraz categories. Opcja ta przydaje się w przypadku, gdy operujemy na dużej ilości tabel, gdzie nie zawsze jesteśmy w stanie ich wszystkich zapamiętać.

W zasadzie można uznać, że aplikacja jest gotowa i po uruchomieniu serwera wewnętrznego komendą bin\cake server (jeśli nie wykonano tego wcześniej), pod adresem http://localhost:8765/kontroler, gdzie kontroller to: articles lub categories zostanie ona uruchomiona i pozwoli na dodawanie, edycję oraz usuwanie artykułów oraz kategorii.

Otwórz więc stronę http://localhost:8765/categories w przeglądarce. Efektem powinno być wyświetlenie zaplecza kategorii:

CakePHP wygenerował tzw. CRUD, czyli gotowy model wraz z kontrolerem i niezbędnymi metodami. Spróbuj teraz użyć dostępnych linków i przetestować działanie aplikacji. Z pewnością wszystko będzie działać poprawnie. Dodaj kilka przykładowych kategorii za pomocą linku New Category.

Jak widzisz, CakePHP stworzył w pełni funkcjonalny system bazodanowy, bez wpisywania żadnej linii kodu. Co więcej – bake zidentyfikował powiązania pomiędzy tabelami i dodał także link List Articles oraz New Article do operowania na tabeli artykuły. Teraz, gdy będziesz dodawał lub edytował artykuł, o ile nie popełniono błędu przy tworzeniu tabel, zostanie wyświetlony dropdown z listą dostępnych kategorii (należy wcześniej wpisać kilka przykładowych kategorii). Również w formularzu artykułów będą odnośniki do kategorii.

W zasadzie prosty system można by uznać za gotowy, jednak cechuje się on jeszcze pewnymi niedoskonałościami. Po pierwsze chcielibyśmy, by komunikaty i formularze komunikowały się w języku polskim. Po drugie, na formularzu artykułu mamy wyświetlone pole counter, które później będzie służyć jako licznik odwiedzin artykułu, jednak pole to powinno być niedostępne przy tworzeniu czy edycji artykułu, zaś jego wartość powinna się zwiększać podczas każdego wyświetlenia danego artykułu.

Na początek spolonizujmy formularze.

Wygenerowane formularze znajdują się w katalogu src\Template, w którym automatycznie zostały utworzone foldery o nazwach poszczególnych modeli. Aby spolonizować formularz artykuły, przejdź do folderu Articles, gdzie ujrzysz 4 wygenerowane pliki o rozszerzeniu CTP. Są to szablony, które są zwykłymi plikami HTML, jednak CakePHP traktuje pliki ctp jako szablony.

Na początek plik index.ctp, który generuje odnośniki oraz listę wszystkich artykułów. Jak widać, wszystkie komunikaty są w języku angielskim. Polonizację można wykonać na dwa sposoby: pierwszy sposób to wygenerowanie pliku lokalizacji, zaś drugi, to ręczna zmiana komunikatów. W tym przykładzie wykorzystamy sposób drugi, bowiem językiem głównym naszej aplikacji będzie język polski. Zostawimy jednak „furtkę”, która umożliwi w przyszłości automatyczne tłumaczenie aplikacji na inne języki.

Poniżej prezentuję oryginalny kod pliku add.ctp, który wyświetla formularz dodawania artykułu:

<nav class="large-3 medium-4 columns" id="actions-sidebar">
    <ul class="side-nav">
        <li class="heading"><?= __('Actions') ?></li>
        <li><?= $this->Html->link(__('List Articles'), ['action' => 'index']) ?></li>
        <li><?= $this->Html->link(__('List Categories'), ['controller' => 'Categories', 'action' => 'index']) ?></li>
        <li><?= $this->Html->link(__('New Category'), ['controller' => 'Categories', 'action' => 'add']) ?></li>
    </ul>
</nav>
<div class="articles form large-9 medium-8 columns content">
    <?= $this->Form->create($article) ?>
    <fieldset>
        <legend><?= __('Add Article') ?></legend>
        <?php
            echo $this->Form->control('title');
            echo $this->Form->control('body');
            echo $this->Form->control('category_id', ['options' => $categories, 'empty' => true]);
            echo $this->Form->control('counter');
        ?>
    </fieldset>
    <?= $this->Form->button(__('Submit')) ?>
    <?= $this->Form->end() ?>
</div>

Wskazówka: w wierszu: echo $this->Form->control(‚category_id’, [‚options’ => $categories, ‚empty’ => true]); można zmienić empty na false. Spowoduje to, że podczas dopisywania nowego artykułu, będzie domyślnie wyświetlona pierwsza kategoria z tabeli categories. Zwykle jako pierwszą kategorię dodaję: Brak kategorii albo Nieprzypisana. Dzięki temu, podczas dodawania nowego artykułu, wyświetla się ona domyślnie na formularzu dodawania artykułu.

Wszystkie wyświetlane użytkownikowi komunikaty znajdują się w specjalnych znacznikach: __(‚Actions’), __(‚List Articles’), __(‚Submit’). Dwa dolne podkreślenia oraz nawias identyfikują cake’a, że jest to łańcuch znaków, który może podmienić na inny (w innym języku). Wystarczy więc, że zamienimy tekst znajdujący się w cudzysłowie i odświeżymy stronę, aby uzyskać polskie komunikaty. Należy się zamienić wszystkie komunikaty na polskie:

<ul class="side-nav">
 <li class="heading"><?= __('Operacje') ?></li>
 <li><?= $this->Html->link(__('Lista artykułów'), ['action'=>'index']) ?></li>
 <li><?= $this->Html->link(__('Lista kategorii'), ['controller'=>'Categories', 'action'=>'index']) ?></li>
 <li><?= $this->Html->link(__('Dodaj kategorię'), ['controller'=>'Categories', 'action'=>'add']) ?></li>
</ul>

Jeżeli chodzi o formularz, sprawa wygląda nieco inaczej, bowiem do tłumaczenia należy zastosować tablicę oraz właściwość label (edytkieta). Poniżej przykład przetłumaczonego formularza.

<?= $this->Form->create($article) ?>
 <fieldset>
 <legend><?= __('Dodawanie nowego artykułu') ?></legend>
  <?php
    echo $this->Form->control('title',['label'=>__('Tytuł artykułu')]);
    echo $this->Form->control('body',['label'=>__('Treść artykułu')]);
    echo $this->Form->control('category_id', ['label'=>__('Wybierz kategorię z listy'), 'options' => $categories, 'empty' => true]);
    //echo $this->Form->control('counter');
  ?>
  </fieldset>
  <?= $this->Form->button(__('Zapisz')) ?>
  <?= $this->Form->end() ?>

Jak widać tłumaczenie nie powinno nastręczyć problemów, zaś jedynym problemem jest chasochłonność tego procesu. Jak widać zakomentowałem przy okazji kontrolkę counter, by była ona niedostępna w formularzu.

Wskazówka: jeżeli jesteś pewien, że aplikacja w przyszości nie będzie tłumaczona na inne języki, można to jeszcze bardziej uprościć, likwidując znaczniki __(”) . W tym przypadku gotowa postać pliku index.ctp będzie wyglądać tak:

<nav class="large-3 medium-4 columns" id="actions-sidebar">
    <ul class="side-nav">
      <li class="heading"><?= 'Operacje' ?></li>
      <li><?= $this->Html->link('Lista artykułów', ['action' => 'index']) ?></li>
      <li><?= $this->Html->link('Lista kategorii', ['controller' => 'Categories', 'action' => 'index']) ?></li>
      <li><?= $this->Html->link('Dodaj kategorię', ['controller' => 'Categories', 'action' => 'add']) ?></li>
    </ul>
</nav>
<div class="articles form large-9 medium-8 columns content">
 <?= $this->Form->create($article) ?>
  <fieldset>
   <legend><?= 'Dodawanie nowego artykułu' ?></legend>
    <?php
      echo $this->Form->control('title',['label'=>'Tytuł artykułu']);
      echo $this->Form->control('body',['label'=>'Treść artykułu']);
      echo $this->Form->control('category_id', ['label'=>'Wybierz kategorię z listy', 'options' => $categories, 'empty' => true]);
       //echo $this->Form->control('counter');
     ?>
    </fieldset>
 <?= $this->Form->button('Zapisz') ?>
 <?= $this->Form->end() ?>
</div>

W powyższym przypadku po prostu pominiąłem znaczniki tłumaczące. Kod wygląda bardziej czytelnie, co jest istotne dla początkujących programistów CakePHP.

Zajmiemiy się teraz polonizacją pliku index.ctp. Nie ma znaczenia którego modelu on dotyczy, bowiem wszystkie wyglądają identycznie, zaś różnią się jedynie kolumnami. Oto oryginalna zawartośc tego pliku:

<?php
/**
  * @var \App\View\AppView $this
  */
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
    <ul class="side-nav">
        <li class="heading"><?= __('Actions') ?></li>
        <li><?= $this->Html->link(__('New Article'), ['action' => 'add']) ?></li>
        <li><?= $this->Html->link(__('List Categories'), ['controller' => 'Categories', 'action' => 'index']) ?></li>
        <li><?= $this->Html->link(__('New Category'), ['controller' => 'Categories', 'action' => 'add']) ?></li>
    </ul>
</nav>
<div class="articles index large-9 medium-8 columns content">
    <h3><?= __('Articles') ?></h3>
    <table cellpadding="0" cellspacing="0">
        <thead>
            <tr>
                <th scope="col"><?= $this->Paginator->sort('id') ?></th>
                <th scope="col"><?= $this->Paginator->sort('title') ?></th>
                <th scope="col"><?= $this->Paginator->sort('created') ?></th>
                <th scope="col"><?= $this->Paginator->sort('modified') ?></th>
                <th scope="col"><?= $this->Paginator->sort('category_id') ?></th>
                <th scope="col"><?= $this->Paginator->sort('counter') ?></th>
                <th scope="col" class="actions"><?= __('Actions') ?></th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($articles as $article): ?>
            <tr>
                <td><?= $this->Number->format($article->id) ?></td>
                <td><?= h($article->title) ?></td>
                <td><?= h($article->created) ?></td>
                <td><?= h($article->modified) ?></td>
                <td><?= $article->has('category') ? $this->Html->link($article->category->name, ['controller' => 'Categories', 'action' => 'view', $article->category->id]) : '' ?></td>
                <td><?= $this->Number->format($article->counter) ?></td>
                <td class="actions">
                    <?= $this->Html->link(__('View'), ['action' => 'view', $article->id]) ?>
                    <?= $this->Html->link(__('Edit'), ['action' => 'edit', $article->id]) ?>
                    <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $article->id], ['confirm' => __('Are you sure you want to delete # {0}?', $article->id)]) ?>
                </td>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
    <div class="paginator">
        <ul class="pagination">
            <?= $this->Paginator->first('<< ' . __('first')) ?>
            <?= $this->Paginator->prev('< ' . __('previous')) ?>
            <?= $this->Paginator->numbers() ?>
            <?= $this->Paginator->next(__('next') . ' >') ?>
            <?= $this->Paginator->last(__('last') . ' >>') ?>
        </ul>
        <p><?= $this->Paginator->counter(['format' => __('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')]) ?></p>
    </div>
</div>

Jak widać bake wygenerował tabelę, ale dodatkowo domyślnie wygenerował paginator, który podzieli tabelę na części w przypadku, gdy ilość artykułów znacznie wzrośnie. Oto gotowy przetłumaczony na język polski plik index.ctp, w którym usunąłem także

<?php
/**
  * @var \App\View\AppView $this
  */
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
    <ul class="side-nav">
        <li class="heading"><?= __('Operacje') ?></li>
        <li><?= $this->Html->link(__('Dodaj artykuł'), ['action' => 'add']) ?></li>
        <li><?= $this->Html->link(__('Lista kategorii'), ['controller' => 'Categories', 'action' => 'index']) ?></li>
        <li><?= $this->Html->link(__('Dodaj kategorię'), ['controller' => 'Categories', 'action' => 'add']) ?></li>
    </ul>
</nav>
<div class="articles index large-9 medium-8 columns content">
    <h3><?= __('Lista artykułów') ?></h3>
    <table cellpadding="0" cellspacing="0">
      <thead>
        <tr>
         <th scope="col"><?= $this->Paginator->sort('id','Ident') ?></th>
         <th scope="col"><?= $this->Paginator->sort('title','Tytuł') ?></th>
         <th scope="col"><?= $this->Paginator->sort('created','Utworzono') ?></th>
         <th scope="col"><?= $this->Paginator->sort('modified','Zmodyfikowano') ?></th>
         <th scope="col"><?= $this->Paginator->sort('category_id','Kategoria') ?></th>
         <th scope="col"><?= $this->Paginator->sort('counter','Ilość odwiedzin') ?></th>
         <th scope="col" class="actions"><?= __('Akcje') ?></th>
        </tr>
        </thead>
        <tbody>
      <?php foreach ($articles as $article): ?>
        <tr>
          <td><?= $this->Number->format($article->id) ?></td>
          <td><?= h($article->title) ?></td>
          <td><?= h($article->created) ?></td>
          <td><?= h($article->modified) ?></td>
          <td><?= $article->has('category') ? $this->Html->link($article->category->name, ['controller' => 'Categories', 'action' => 'view', $article->category->id]) : '' ?></td>
          <td><?= $this->Number->format($article->counter) ?>
          <td class="actions">
          <?= $this->Html->link(__('Pogląd'), ['action'=>'view', $article->id]) ?>
          <?= $this->Html->link(__('Edytuj'), ['action'=>'edit', $article->id]) ?>
          <?= $this->Form->postLink(__('Usuń'), ['action'=>'delete', $article->id], ['confirm'=>__('Czy na pewno chcesz usunąć artykuł {0}?', $article->title)]) ?>
                </td>
         </tr>
       <?php endforeach; ?>
        </tbody>
    </table>
    <div class="paginator">
        <ul class="pagination">
        <?= $this->Paginator->first('<< ' . __('pierwszy')) ?>
        <?= $this->Paginator->prev('< ' . __('poprzedni')) ?>
        <?= $this->Paginator->numbers() ?>
        <?= $this->Paginator->next(__('następny') . ' >') ?>
        <?= $this->Paginator->last(__('ostatni') . ' >>') ?>
        </ul>
        <p><?= $this->Paginator->counter(['format' => __('Strona {{page}} z {{pages}}, wyświetla się {{current}} rekord(ów) z {{count}} dostępnych')]) ?></p>
    </div>
</div>

A oto część zrzutu ekranu wyświetlającego listę artykułów (http://localhost/articles).

W zasadzie wszystko działa poprawnie, jednak zauważyć można błędny format dat utworzenia oraz modyfikacji rekordu. Należy więc skonfigurować Cake’a, by używał innego formatu.

Otwieramy plik config/app.php (ten, w którym zmienialiśmy konfigurację połączenia z bazą danych) i odnajdujemy linię ‚defaultLocale’ => env(‚APP_DEFAULT_LOCALE’, ‚en_US’), zmieniając ją na ‚defaultLocale’ => env(‚APP_DEFAULT_LOCALE’, ‚pl_PL’),

Otwieramy plik config/bootstrap.php i odnajdujemy linię date_default_timezone_set(‚UTC’); zastępując ją date_default_timezone_set(‚Europe/Warsaw’);

Dodatkowo w tym samym pliku dopisujemy dwie linie:

Cake\I18n\Date::setToStringFormat(‚yyyy-MM-dd’);
Cake\I18n\FrozenDate::setToStringFormat(‚yyyy-MM-dd’);

Powyższe linie nie są niezbędne, jednak w przyszłości mogą pomóc przy edycji formularzy, gdzie występują kontroli daty lub daty i czasu – szczególnie gdy korzystamy z biblioteki QJuery. Warto więc zrobić to teraz, by CakePHP poprawnie interpretował format daty. Po odświeżeniu strony będzie wyświetlany poprawy format daty oraz czasu, jak na obrazku poniżej:

Kolejnym krokiem jest przetłumaczenie wszystkich szablonów znajdujących się w katalogu Articles oraz Categories. Myślę, że po przeczytaniu opisu dotyczącego tłumaczenia, czytelnik nie będzie miał problemu z polonizacją aplikacji. W dalszej częsci zajmiemy się tłumaczeniem komunikatów informujących o poprawnym zapisaniu, bądź błędzie podczas zapisywania rekordów w bazie danych. Specjalnie w tym miejscu nie będziemy się jeszcze tym zajmować, bowiem komunikaty te znajdują się w kontrolerach, które zostaną opisane w dalszej części.

Technikalia

Teraz trochę techniki, ale bez programowania, bowiem CakePHP sam wygenerował wszystkie niezbędne źródła bloga. Warto jednak poznać bliżej wygenerowane przez bake’a pliki, bowiem czasem trzeba będzie coś zmodyfikować lub ręcznie ulepszyć.

Wszystkie pliki wygenerowanej aplikacji znajdują się w katalogu src, gdzie znajdują się modele, kontrolery i widoki. W tym momencie warto zapoznać się z konwencją nazw Cake’a, o ile jeszcze tego nie zrobiłeś.

Katalog src\Model\Entity zawiera pliki modelu Category oraz Articles. W zasadzie pliki te nie muszą nas interesować, więc ich zawartości omawiać nie będę.

Katalog src\Model\Table jest o wiele ciekawszy i koniecznie należy omówić jego zawartość na przykładzie. Znajdziemy w nim CategoriesTable.php oraz ArticlesTable.php. Warto zauważyć, że ich nazwa (wygenerowana automatycznie przez bake) to nazwa fizycznych tabel w bazie danych.

Na początek plik CategoriesTable.php, który opiszę w partiach. Usunąłem tez komentarze, aby zwiększyć czytelność kodu.

public function initialize(array $config)
    {
        parent::initialize($config);
  1.     $this->setTable('categories');
  2.     $this->setDisplayField('name');
  3.     $this->setPrimaryKey('id');
    }
  1. Określenie tabeli, z której pobierane są dane.
  2. setDisplayField to bardzo istotny wiersz, bowiem określa pole, które będzie się wyświetlać np. przy wyborze kategorii. Np. Jeśli artykuł posiada pole category_id – powiązane z categories->id, to w polu dropdown na formularzu artykułu wartością będzie categories->id, lecz jako opcje wyświetlane będą właśnie pola setDisplayField. Jeśli więc na formularzu artykułu, zamiast nazwy kategorii wyświetla się jedynie id, zmień setDisplayField w tabeli źródła (categories), odśwież stronę, a ujrzysz przyjazną nazwę kategorii.
  3. Określa klucz główny tabeli.
public function validationDefault(Validator $validator)
    {
1.        $validator
2.          ->integer('id')
3.          ->allowEmpty('id', 'create');
 
        $validator
           ->requirePresence('name', 'create')
4.         ->notEmpty('name')
']);
        return $validator;
    }
  1. W tej części mamy validator czyli kontrolę pól. Od tej części zależy, jak będzie zachowywał się formularz, w którym dopisuje się czy edytuje rekord.
  2. Oznaczenie typu oraz nazwy pola w tabeli bazy danych. W tym przypadku jest to pole id, czyli int(11) unsigned, lecz w zależności od typu pola, można tutaj podać inne typy, np. booolean, który w przypadku CakePHP to typ unsigned tinyint(1).
  3. Zaznaczenie czy pole może pozostać puste. Jeśli tak: allowEmpty(‚nazwapola’), jeśli pole musi być wypełnione – notEmpty(‚nazwapola’). Dodatkowy parametr (create) informuje, że kontrola odbywa się podczas tworzenia nowego wiersza, dlatego też w tym przypadku mamy do czynienia z pozwoleniem na pole puste, bowiem to silnik bazy danych zajmuje się wstawianiem wartości pola, bowiem pole id jest oznaczone jako autoinclement.
  4. W przypadku pola name, mamy je oznaczone jako konieczne do wpisania w formularzu, zaś w przypadku pozostawienia pustego pola, Cake wygeneruje informację o konieczności jego wypełnienia dla poprawnego zapisania zmian w tabeli.

Teraz omówię najważniejsze wiersze pliku ArticlesTable.php

class ArticlesTable extends Table
{
    public function initialize(array $config)
    {
      parent::initialize($config);
      $this->setTable('articles');
      $this->setDisplayField('title');
      $this->setPrimaryKey('id');
  1.  $this->addBehavior('Timestamp');
  2.  $this->belongsTo('Categories', ['foreignKey'=>'category_id','joinType'=>'INNER'
      ]);
    }
 
    public function validationDefault(Validator $validator)
    {
        $validator
          ->integer('id')
          ->allowEmpty('id', 'create')
          ->add('id', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);
 
        $validator
          ->requirePresence('title', 'create')
          ->notEmpty('title');
 
        $validator
          ->requirePresence('body', 'create')
          ->notEmpty('body');
 
        return $validator;
    }
 
    public function buildRules(RulesChecker $rules)
    {
 3.  $rules->add($rules->isUnique(['id']));
 4.  $rules->add($rules->existsIn(['category_id'], 'Categories'));
     return $rules;
    }
}
  1. Wiersz dodający behawior timestamp generowany jest wówczas, gdy bake odkryje istnienie pól created i modified. Jest to specjalny i niezwykle pożyteczny mechanizm, który przy zapisywaniu rekordu będzie automatycznie uzupełniał datę utworzenia oraz modyfikacji rekordu.
  2. Tworzenie relacji pomiędzy tabelą articles a categories. W dosłownym znaczeniu belongsTo oznacza „należy do”. Czyli w tym przypadku mamy tutaj do czynienia ze złączeniem typu INNER tabeli articles->categories, za którą odpowiada klucz obcy category_id, zaś domyślnie złączenie następuje do pola id w tabeli categories. (więcej)
  3. Dodanie walidacji unikalności, co oznacza, że wartość pola id nie może się powtórzyć w obrębie całej kolumny.
  4. Dodanie warunku, który zwróci poprawność tylko wówczas, gdy klucz obcy category_id ma swój odpowiednik w polu id w tabeli categories.

Na szczęście kod źródłowy aplikacji został wygenerowany automatycznie. Co więcej – generator bake zdublował mechanizmy walidacji, bowiem jeśli usunęlibyśmy walidacje po stronie kodu, mechanizm walidacji zadziała ze strony bazy danych, jednak należy walidować dane na wielu poziomach – szczególnie w kodzie, bowiem CakePHP będzie w stanie uniemożliwić zapis danych, jeżeli nie zostaną spełnione wszystkie warunki walidacji. W konsekwencji bedzie można uniknąć wyjątków generowanych przez silnik bazy danych, które trudniej jest obsłużyć w kodzie. Warto podkreślić, że częściową walidację można wymusić także w kontrolce widoku, wykorzystując opcję required, np. jeżeli pole bazy danych title nie wymaga wpisywania, a chemy, by użytkownik koniecznie podał tę wartość, można w widoku formularza (add.ctp lub edit.ctp) zmodyfikować:

echo $this->Form->control('title',['label'=>__('Tytuł artykułu')]);

dodając opcję required, jak poniżej:

echo $this->Form->control('title',['required'=>true,label'=>__('Tytuł artykułu')]);

Więcej informacji na temat walidacji danych znaleźć można na stronie CakePHP – Validation.

Teraz opiszę zawartość wygenerowanego kontrolera – CategoryController.php, który znajduje się w katalogu src\Controller. Plik ten został wygenerowany automatycznie i zauważyć można standardowo tworzone metody CRUD: Create(add), Read(view), Update(edit), Delete(delete). Oprócz tego, bake zawsze generuje metodę index, która zawiera listę rekordów wraz z paginatorem, dzielącym w razie konieczności tabelę na części. Kontroler został przeze mnie spolonizowany, więc można go porównać z plikiem oryginalnym.

class CategoriesController extends AppController
{
   public function index()
    {
      $categories = $this->paginate($this->Categories);
      $this->set(compact('categories'));
      $this->set('_serialize', ['categories']);
    }
 
    public function view($id = null)
    {
      $category = $this->Categories->get($id, ['contain' => []]);
      $this->set('category', $category);
      $this->set('_serialize', ['category']);
    }
 
    public function add()
    {
     $category = $this->Categories->newEntity();
     if ($this->request->is('post')) {
     $category = $this->Categories->patchEntity($category, $this->request->getData());
       if ($this->Categories->save($category)) {
         $this->Flash->success(__('Kategoria została zapisana poprawnie.'));
           return $this->redirect(['action' => 'index']);
          }
         $this->Flash->error(__('Nie udało się dodać nowej kategorii.'));
        }
        $this->set(compact('category'));
        $this->set('_serialize', ['category']);
    }
 
    public function edit($id = null)
    {
     $category = $this->Categories->get($id, ['contain' => []]);
      if ($this->request->is(['patch', 'post', 'put'])) {
     $category = $this->Categories->patchEntity($category, $this->request->getData());
       if ($this->Categories->save($category)) {
           $this->Flash->success(__('Kategoria została zapisana poprawnie.'));
             return $this->redirect(['action' => 'index']);
            }
          $this->Flash->error(__('Nie udało się zaktualizować kategorii.'));
        }
        $this->set(compact('category'));
        $this->set('_serialize', ['category']);
    }
 
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        $category = $this->Categories->get($id);
        if ($this->Categories->delete($category)) {
        $this->Flash->success(__('Kategoria została usunięta poprawnie.'));
        } else {
       $this->Flash->error(__('Nie udało się usunąć kategorii.'));
        }
        return $this->redirect(['action' => 'index']);
    }
}

Nie będę się na razie zagłębiał w kod, bowiem na dzień dzisiejszy jest to niepotrzebne. Choć istnieje tu kilka linii, które można bez konsekwencji usunąć, nie będę się tym zajmować. Ufam, że kod jest poprawny i działa jak trzeba, zaś aplikacja jest gotowa do działania.

Pytanie, sugestie, zgłaszanie błędów: leszek.klich@gmail.com

Ciąg dalszy nastąpi…

2173total 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 *

+ 14 = 23

This site uses Akismet to reduce spam. Learn how your comment data is processed.