Drupal 8 - modules migration
Port modułu z Drupala 7 na 8 na przykładzie modułu Cookiec

Od premiery Drupala 8 minęło już trochę czasu. Ciągle pojawiają się nowe projekty oraz moduły, na drupal.org coraz więcej projektów jest dostosowanych do wersji 8.x. Niestety często są to wersje nie do końca stabilne oraz pozbawione bugów. Ilość dodatkowych modułów, które możemy zainstalować na Drupal 8 wynosi na dziś około 2250, w porównaniu do 12400 na Drupala 7. Gołym okiem widać, że mamy dużo węższą gamę gotowych rozwiązań niż w przypadku starszej generacji. O ile w przypadku zaawansowanych projektów pisanie ich od nowa może mijać się z celem, to w przypadku niewielkich dodatków możemy pokusić się o przeniesienie całego modułu lub interesujących nas funkcjonalności na wersje 8.x. W tym poradniku przeniesiemy na Drupala 8 moduł, który wykorzystujemy do wyświetlania informacji o cookies na naszej stronie.

struktura plikow drupal 7

Tak wygląda struktura plików na wersje 7.x. Moduł jest dość prosty zawiera customowy plik css, skrypt js, dwie templatki, jeden includowany .inc oraz standardowe .info oraz .module.

Aby skrypt był widoczny przez Drupala 8 potrzebujemy plik .info.yml , zaczniemy więc od niego. W D8 jest to jedyny plik wymagany, aby moduł był widoczny przez system.

 

1. Pliki info

Drupal 7

cookiec.info

name = CookieC
description = This module aims at making the website compliant with the new EU cookie regulation
core = 7.x

Drupal 8

cookiec.info.yml

name: CookieC
type: module
core: 8.x
version: 8.x-1.0
description: This module aims at making the website compliant with the new EU cookie regulation
configure: cookiec.settings

Pliki są dość podobne - jak widać, podstawową różnicą jest format plików, w Drupalu 8 jest to YML, przez co rozszerzenie musi być .info.yml. Dodatkowo musimy wprowadzić type : gdyż skórki przyjmują podobną składnie, w naszym wypadku będzie to type: modul. Dodatkowo w tym pliku możemy dodać ‘configure:’ jest to routing do strony konfiguracyjnej, ale o routingu powiemy w następnej części. Warto wspomnieć, iż w plikach .yml podobnie jak np. w Pythonie ważne jest zachowanie wcięć w kodzie. Po wyczyszczeniu cache moduł powinien być już dostępny na liście.

Modul extend

 

2. Plik .module oraz hooki

Zobaczmy teraz co kryje plik module w wersji D7. W naszym module zostały użyte następujące hooki/funkcje.

  • hook_menu – użyty do zdefiniowania strony /cookiec, na której wyświetlamy politykę cookie,
  • hook_init – do zainicjowania naszej funkcjonalności, między innymi tutaj znajduje się funkcja ładująca customowy plik css i js,
  • hook_permission – uprawnienia do administracji naszym modułem,
  • hook_theme – definicje templates.

W D8 hook_menu, hook_permission oraz hook_init zostały usunięte.

Zadania hook_menu przejął plik  "nazwa_modulu.routing.yml", a zamiast hook_init użyjemy EventSubscriber.

 

3. Migracja hook_menu

Drupal 7

Hook menu zawarty w pliku .module

<?php
/**
 * Implements hook_menu().
 */
function cookiec_menu() {
  $items['cookiec'] = array(
    //'title' => '',
    'description' => 'Cookie policy page.',
    'page callback' => 'cookiec_policy_page',
    'access arguments' => array('access content'),
    'file' => 'cookiec.page.inc',
  );
  return $items;
}

Drupal 8

zawartość pliku cookiec.routing.yml

cookiec.render_cookiec:
  path: '/cookiec'
  defaults:
    _controller: '\Drupal\cookiec\Controller\Cookiec::renderPage'
    _title: ''
  requirements:
    _permission: 'access content'

Plik *.routing.yml zawiera nazwę routingu - cookiec.render_cookiec.

Path : czyli adres URL, pod którym będziemy mieli dostęp do danej funkcjonalności; tak jak w D7 możemy używać ścieżek dynamicznych, czyli np. path: 'example/{user}', w tym wypadku nie będzie potrzebny, gdyż nasz moduł wyświetla jedynie stronę statyczną.

defaults: _controller: '\Drupal\cookiec\Controller\Cookiec::renderPage' pod adresem /cookiec będzie wyświetlona zawartość, którą zwróci klasa Cookiec metodą renderPage(). O tym napiszemy przy tworzeniu strony statycznej /cookiec.

Requirements: Czyli wymagania, my dodajemy ustawienia dostępu w _permission: access content.

Powinniśmy od razu utworzyć wszystkie klasy i metody przypisane do naszych routingów, aby uniknąć wyświetlania błędów.

Utworzymy  "Hello Word", stworzymy zatem plik Cookiec.php w katalogu /src/Contoller:

namespace Drupal\cookiec\Controller;

use Drupal\Core\Controller\ControllerBase;

class Cookiec extends ControllerBase {

  function renderPage(){

    return array(
      '#title' => '',
      '#markup' => 'hello word!',
    );
  }
}

Po oczyszczeniu cache oraz wejściu na stronę /cookies powinniśmy otrzymać stronę z napisem Hello Word.

 

4. Migracja hook_permissions

Aby stworzyć własne uprawnienia tworzymy plik module.permission.yml. podobnie jak inne pliki .yml,

administer cookiec:
  title: 'Administer cookiec administration message module'
  description: 'Perform administration tasks for cookiec'

w routingu dodajemy natomiast

requirements:
  _permission: 'administer cookiec'

Przydatnym rozwiązaniem w tym wypadku może być użycie klasy formularza.

cookiec.settings:
  path: '/admin/config/content/cookiec-settings'
  defaults:
    _form: '\Drupal\cookiec\Forms\CookiecSettingsForm'
    _title: 'cookiec configuration'
  requirements:
    _permission: 'administer cookiec'

Nazwę routingu cookiec.settings umieścimy właśnie w pliku .info.yml (configure: cookiec.settings), przez co przycisk konfiguracji modułu przekieruje nas właśnie na ten formularz.

 

5. Migracja hook_init

Hook_init, który w D7 odpalał się przy każdym ładowaniu strony, został usunięty. Aby uzyskać podobną funkcjonalność użyjemy Eventów, a dokładniej EventSubscriber.

Aby utworzyć EventSubscriber w Drupalu 8 potrzebować będziemy service. Service tworzymy podobnie jak inne .yml.

w pliku cookiec.service.yml dodajemy

services:
  cookiec_event_subscriber:
    class: Drupal\cookiec\EventSubscriber\CookiecSubscriber
    tags:
      - {name: event_subscriber}

aby nasz service był Event Subscriberem musimy stworzyć klasę implementująca interface EventSubscriberInterface, Do poprawnego działania stworzymy 3 metody:

w pliku src/EventSubscriber/CookiecSubscriber.php

/**
 * @file Drupal\coociec\EventSubscriber\PopupMessageSubscriber
 */
namespace Drupal\cookiec\EventSubscriber;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;


/**
 * Class PopupMessageSubscriber
 * @package Drupal\cookiec\EventSubscriber
 */
class CookiecSubscriber implements EventSubscriberInterface {

protected $config;
  /**
   * CookiecSubscriber constructor.
   */
  public function __construct() {
    $this->config = \Drupal::configFactory()->get('cookiec.settings');
  }

  public function showCookiecMessage(FilterResponseEvent $event) {
    // Check permissions to display message.
    $response = $event->getResponse();

    if (!$response instanceof AttachmentsInterface) {
      return;
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = array('showCookiecMessage', 20);

    return $events;
  }
}

W naszym konstruktorze ładujemy ustawienia modułu, które później przekażemy do skryptu JavaScript.

Aby nasz Event działał, dodajemy metodę getSubscribedEvents(), podajemy nazwę metody (showCookiecMessage), która będzie wywoływana oraz wagę (20). Wagą możemy ustawiać kolejność eventów.

  public static function getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = array('showCookiecMessage', 20);

    return $events;
  }

 

6. Dołączanie plików JS oraz CSS

Następnym krokiem będzie przetransferowanie plików JS oraz CSS, które w naszym przypadku zostawimy bez większych zmian, skupimy się na ich ładowaniu podczas wykonywania kodu.

Gdy chcemy dołączyć zewnętrze pliki JS lub CSS do naszego modułu, możemy stworzyć plik o nazwie module.libraries.yml, w naszym wypadku będzie to cookiec.libraries.yml

cookiec_library:
  version: 1.x
  css:
    theme:
      css/cookiec.css: {}
  js:
    js/cookiec.js: {preprocess: false}

Dodaliśmy pliki cookiec.css oraz cokiec.js, jeżeli potrzebujemy załadować już istniejące biblioteki, możemy dodać je poprzez dependencies np:

  dependencies:
    - core/jquery

 

Aby załadować nasze biblioteki, możemy skorzystać np. z hook_preprocess_HOOK, wtedy do variables dodajemy:

$variables['#attached']['library'][] = 'cookiec/cookiec_library';

W naszym przykładzie dodamy nasze pliki bezpośrednio przy załadowaniu eventu. Użyjemy do tego następujących metod:

    $response = $event->getResponse();
    $attachments = $response->getAttachments();
    $attachments['library'][] = 'cookiec/cookiec_library';
    $response->setAttachments($attachments);

Nasz moduł dużą część działania wykonuje po stronie przeglądarki użytkownika. Nasz skrypt do poprawnego działania wymaga przesłaniu kilku parametrów. Opcje takie jak wysokość, szerokość, wyświetlany tekst czy pozycja są wysyłane do JSowej tablicy  drupalSettings.

    $variables = array(
      'popup_enabled' => $config->get('popup_enabled'),
      'popup_agreed_enabled' => $config->get('popup_agreed_enabled'),
      'popup_hide_agreed' => $config->get('popup_hide_agreed'),
      'popup_height' => $config->get('popup_height'),
      'popup_width' => $config->get('popup_width'),
      'popup_delay' => $config->get('popup_delay')*1000,
      'popup_link' => $config->get($language."_link"),
      'popup_position' => $config->get('popup_position'),
      'popup_language' => $language,
      'popup_html_info' => $html_info,
      'popup_html_agreed' =>$html_agreed,
    );

Wartości zmiennych pobierane są z ustawień modułu, czyli configów, więcej o tym powiemy za chwilę.

Tak oto przekazujemy zmienne php do JS.

    $attachments['drupalSettings']['cookiec'] = $variables;

$html_info oraz $html_agreed przechowują kod html, który został uzyskany przez parsowanie templatek TWIG oraz zmiennych:

    $variables =  array(
      'title' => 'title',
      'message' => $config->get($language."_popup_info"),
    );

    $twig = \Drupal::service('twig');
    $template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_info.html.twig');
    $html_info = $template->render($variables);


    $variables =  array(
      'title' => 'title',
      'message' => $config->get($language."_popup_info"),
      'more' => 't(more)',
      'hide' => 't(hide)',
    );
    $twig = \Drupal::service('twig');
    $template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_agreed.html.twig');
    $html_agreed = $template->render($variables);

Więcej o TWIGU powiemy za chwilę.

 

Cały plik z EventSubscriber wygląda tak:

<?php

/**
 * @file Drupal\coociec\EventSubscriber\PopupMessageSubscriber
 */
namespace Drupal\cookiec\EventSubscriber;

use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\Element;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;


/**
 * Class PopupMessageSubscriber
 * @package Drupal\popup_message\EventSubscriber
 */
class CookiecSubscriber implements EventSubscriberInterface {

  /**
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $config;

  /**
   * PopupMessageSubscriber constructor.
   */
  public function __construct() {
    $this->config = \Drupal::configFactory()->get('cookiec.settings');
  }

  /**
   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
   */
  public function showCookiecMessage(FilterResponseEvent $event) {
    // Check permissions to display message.
    $response = $event->getResponse();

    if (!$response instanceof AttachmentsInterface) {
      return;
    }
    // Check module has enable popup
    $config = $this->config;
    $language = \Drupal::languageManager()->getCurrentLanguage()->getId();

    $variables =  array(
      'title' => 'title',
      'message' => $config->get($language."_popup_info"),
    );

    $twig = \Drupal::service('twig');
    $template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_info.html.twig');
    $html_info = $template->render($variables);


    $variables =  array(
      'title' => 'title',
      'message' => $config->get($language."_popup_info"),
      'more' => 'more',
      'hide' => 'hide',
    );
    $twig = \Drupal::service('twig');
    $template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_agreed.html.twig');
    $html_agreed = $template->render($variables);



    $variables = array(
      'popup_enabled' => $config->get('popup_enabled'),
      'popup_agreed_enabled' => $config->get('popup_agreed_enabled'),
      'popup_hide_agreed' => $config->get('popup_hide_agreed'),
      'popup_height' => $config->get('popup_height'),
      'popup_width' => $config->get('popup_width'),
      'popup_delay' => $config->get('popup_delay')*1000,
      'popup_link' => $config->get($language."_link"),
      'popup_position' => $config->get('popup_position'),
      'popup_language' => $language,
      'popup_html_info' => $html_info,
      'popup_html_agreed' =>$html_agreed,
    );

    $attachments = $response->getAttachments();
    $attachments['library'][] = 'cookiec/cookiec_library';
    $attachments['drupalSettings']['cookiec'] = $variables;
    $response->setAttachments($attachments);
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::RESPONSE][] = array('showCookiecMessage', 20);

    return $events;
  }
}

7. Configi

Dużą zmianą w Drupalu 8 jest wykorzystanie plików config. Są to także pliki YML, które mają ułatwić synchronizacje ustawień, variable i innych danych zapisanych w bazie pomiędzy środowiskami.
Więcej na ten temat znajdziecie pod https://www.drupal.org/docs/8/configuration-management/managing-your-sites-configuration

W naszej wersji modułu na Drupala 8 configi przetrzymują ustawienia naszego okna oraz ładują domyślne ustawienia podczas instalacji. Jeżeli umieścimy plik z configami w folderze config/install zostaną one automatyczne wczytane przy instalacji modułu, nie musimy używać hook_install.

nasz plik config/install/cookiec.settings.yml

popup_enabled: 1
popup_agreed_enabled: 1
popup_hide_agreed: 1
popup_width: 100%
popup_delay: '1'
popup_height: '100'
en_popup_title: 'Cookie policy'
en_popup_info: 'This website uses cookies. By remaining on this website you agree to our <a href="/cookiec">cookie policy</a>'
en_popup_agreed: 'I agree'
en_popup_p_private: " <p>This website does not automatical....</p>"
pl_popup_title: 'Polityka cookie'
pl_popup_info: 'Powiadomienie o plikach cookie. Ta strona korzysta z plików cookie. Pozostając na tej stronie, wyrażasz zgodę na korzystanie z plików cookie. <a href="/cookiec">Dowiedz się więcej'
pl_popup_agreed: 'Zgadzam się'
pl_popup_p_private: "    <p>Serwis nie zbiera w sposób automatyczny żadnych informacji, z wyjątkiem informacji zawartych w plikach cookies.</p>\r\n    <p>Pliki cookies (tzw. „ciasteczka”) </p>"
en_popup_link: /cookiec
pl_popup_link: /cookiec

Jeżeli w kodzie strony potrzebujemy pobrać te dane, używamy service o nazwie configFactory() z metoda get oraz podajemy nazwę configu.

   $this->config = \Drupal::configFactory()->get('cookiec.settings');

Ten kod wykorzystany został w konstruktorze klasy CookiecSubsciber, przez co mamy wszystkie ustawienia modułu pod ręką.
W naszym kodzie przypisujemy je do tablicy $variables

...
 'popup_hide_agreed' => $config->get('popup_hide_agreed'),
 'popup_height' => $config->get('popup_height'),
 'popup_width' => $config->get('popup_width'),
...

 

8. Templatki TWIG

Jedną z większych zmian, które odczujemy po przesiadce z 7 na 8 jest wymiana PHP templates na TWIG (http://twig.sensiolabs.org/). Jest to temat na tyle długi, iż pewnie przygotujemy o tym osobny artykuł. Dla nas ważne jest, że nie możemy używać już funkcji PHP, a logika ogranicza się do prostych pętli i warunków.

Nasz moduł dla Drupla 7 posiada dwie templatki:

cookiec-agreed.tpl.php

cookiec-info.tpl.php

<?php
/**
 * @file
 * This is a template file for a pop-up informing a user that he has already
 * agreed to cookies.
 *
 * When overriding this template it is important to note that jQuery will use
 * the following classes to assign actions to buttons:
 *
 * hide-popup-button - destroy the pop-up
 * find-more-button  - link to an information page
 *
 * Variables available:
 * - $message:  Contains the text that will be display whithin the pop-up
 */
?>

<div>
  <div class ="popup-content agreed">
    <div id="popup-text">
      <?php print $message ?>
    </div>
    <div id="popup-buttons">
      <button type="button" class="hide-popup-button"><?php print t("Hide this message"); ?> </button>
      <button type="button" class="find-more-button" ><?php print t("More information on cookies"); ?></button>
    </div>
  </div>
</div>

Zaczniemy od zamiany nazwy na poprawne, rozszerzenie podmieniamy na xxx.html.twig.

W plikach .twig tagi php nie są parsowane, wszystkie funkcje jak i komentarze musimy dostosować do TWIG'a.

Komentarze:

jeżeli chcemy zachować lub dodać nowe komentarze, zamieniamy tagi PHP na {# ...#}

<#
/**
 * @file
 * This is a template file for a pop-up informing a user that he has already
 * agreed to cookies.
 *
 * When overriding this template it is important to note that jQuery will use
 * the following classes to assign actions to buttons:
 *
 *
 * Variables available:
 * message  Contains the text that will be display whithin the pop-up
 * hide - destroy the pop-up
 * more  - link to an information page
 */
#>

Drukowanie zawartości zmiennych

Drukowanie zmiennych uzyskujemy poprzez umieszczenie naszej zmiennej w nawiasach {{ zmienna }}. W naszym module mamy dostępne trzy zmienne:
message, hide, more - zawierają przetłumaczone stringi. Dodanie {{ message | raw }} powoduje, iż html będzie renderowany w czystej postaci bez zamieniania np. < > na &lt; &gt; .

<div>
  <div class ="popup-content agreed">
    <div id="popup-text">
      {{ message | raw}}
    </div>
    <div id="popup-buttons">
      <button type="button" class="hide-popup-button">  {{ hide }} </button>
      <button type="button" class="find-more-button" >  {{ more }} </button>
    </div>
  </div>
</div>

Logika w TWIGU

Nasz przykład jest dość prosty, natomiast TWIG pozwala na używanie nieskomplikowanej logiki. Operacje logiczne opakowujemy w tagi {% %}

Możemy korzystać między innymi z tagów, filtrów zmiennych oraz funkcji.

Kilka przykładów na początek:

Przykładowe tagi

Pętla foreach

    {% for user in users %}
        <li>{{ user.username|e }}</li>
    {% endfor %}

Warunek IF

{% if online == false %}
    <p>Our website is in maintenance mode. Please, come back later.</p>
{% endif %}

Operacje na zmiennych

{% set foo = 'bar' %}

 

Przykładowe filtry

Filtrów używamy poprzez dodanie  | w {{ }} z naszą zmienną.

Trim - usuwa whitespace lub podane stringi

{{ '  I like Twig.  '|trim }}
{# outputs 'I like Twig.' #}

{{ '  I like Twig.'|trim('.') }}
{# outputs '  I like Twig' #}

Date - formatowanie daty

{{ "now"|date("m/d/Y") }}
{{ post.published_at|date("m/d/Y", "Europe/Paris") }}

Funkcje

Funkcja random ()

{{ random(['apple', 'orange', 'citrus']) }} {# example output: orange #}
{{ random('ABC') }}                         {# example output: C #}
{{ random() }}                              {# example output: 15386094 (works as the native PHP mt_rand function) #}
{{ random(5) }}                             {# example output: 3 #}

W Drupalu dodana jest także przydatna metoda AddClass - dodanie klas css do elementu html.

{%
  set classes = [
    'red',
    'green',
  ]
%}
<div{{ attributes.addClass(classes) }}></div>

Jest to tylko kilka przykładów, po więcej zapraszam do dokumentacji na http://twig.sensiolabs.org/documentation.

Dodatkowo przy pracy z DRUPAL 8 I TWIG polecam zapoznać się z

Przesyłanie danych do TWIGA

Jak widać TWIG daje dość dużo możliwości, ale aby zmienne były widoczne w naszym TWIGU musimy je tam dostarczyć.

W naszym przykładzie potrzebujemy zapisać zawartość sparsowanego TWIG do zmiennej i wysłać do tablicy z JS. Można to zrobić w taki sposób:

Najpierw zbieramy zmienne, które chcemy wykorzystać w naszym TWIGU.

 $variables =  array(
      'title' => 'title',
      'message' => $config->get($language."_popup_info"),
      'more' => 'more',
      'hide' => 'hide',
    );

Parsowanie template do zmiennej:

    $twig = \Drupal::service('twig');
    $template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_agreed.html.twig');
    $html_agreed = $template->render($variables);

Przy pracy z Drupalem 8, dość rzadko będziemy korzystać z takiego sposobu. Zazwyczaj używamy innych sposobów np. nadpisujemy standardowe templatki używając Twig Template naming conventions.

Bloki:

  1. block--module--delta.html.twig
  2. block--module.html.twig
  3. block.html.twig

Nody:

  1. node--nodeid--viewmode.html.twig
  2. node--nodeid.html.twig
  3. node--type--viewmode.html.twig
  4. node--type.html.twig
  5. node--viewmode.html.twig
  6. node.html.twig

itp

Tak przygotowane customowe templatki wrzucamy do naszego theme/templates.

Bardziej zaawansowana metoda to dodanie plików bezpośrednio do modułów; w takim wypadku używamy hook_theme,

/**
 * Implements hook_theme().
 */
function cookiec_theme() {
  return array(
    'cookiec_agreed' => array(
      'template' => 'cookiec_agreed',
      'variables' => array(
        'title' => NULL,
        'body' => NULL,
        'read_more' => NULL,
      ),
    ),
    'cookiec_info' => array(
      'template' => 'cookiec_info',
      'variables' => array(
        'title' => NULL,
        'body' => NULL,
        'read_more' => NULL,
      ),
    ),
  );
}

Aby użyć takiego TWIGa, nasz blok lub strona musi zwrócić array z kluczem #theme  oraz zmienne zdefiniowane w hook_theme().

Poniżej przykład wykorzystania bloku z customowym twigiem.

namespace Drupal\hello_world\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'Hello' Block
 *
 * @Block(
 *   id = "hello_block",
 *   admin_label = @Translation("Hello block"),
 * )
 */
class HelloBlock extends BlockBase {
  /**
   * {@inheritdoc}
   */
  public function build() {

 $variables =  array(
      'title' => 'title',
      'body' => 'body',
      'read_more' => 'more',
    );

    return array(
      '#theme' => 'cookiec_info',
      '#variables' => $variables,
    );
  }
}

 

9. Podsumowanie

Wszystkie funkcjonalności zostały przeniesione i są kompatybilne z wersją 8.x. Moduł działa i jest używany przez nas na paru projektach.

Projekt możecie pobrać z githuba:

https://github.com/droptica/cookiec/

W razie pytań i problemów zachęcamy do zostawiania komentarzy.

Podsumowując nasz krótki artykuł. Zmiany z D7 na D8 są znaczne i niestety to tylko mała cześć ogromu nowości i możliwości naszego nowego CMS 'a. Jeżeli chcesz dalej zgłębiać wiedzę na temat ósemki oraz innych narzędzi przydatnych przy projektowaniu aplikacji webowych, polub nas na Facebooku gdzie udostępniamy tutoriale, poradniki oraz ciekawostki z branży, bierz udział w Drupal Day oraz Drupal Camp! Już niebawem następne nowości u nas na blogu.

 

Porozmawiajmy o Twoich projektach

Napisz do nas!