Nuxt.js – Wprowadzenie

Witajcie po wakacyjnej przerwie. W dzisiejszym wpisie chcę się z Wami podzielić kilkoma moimi obserwacjami, które napotkałem ucząc się Nuxt.js i Vue przez właśnie wakacje.

Jak wygląda struktura plików w Nuxt.js?

  • Assets -> pliki takie jak SASS, LESS lub JavaScript.
  • Components -> reużywalne komponenty. np. navBar.vue.
  • Layout -> definicje wyglądu różnych elementów na stronie.
  • Middleware -> przechowuje funkcje, które mają się wykonać przed renderowaniem strony lub grupy stron.
  • Pages -> pliki vue opisujące działanie konkretnej strony.
  • Plugins -> kod JS, który ma się wykonać przed renderowaniem komponentów i stron.
  • nuxt.config.js -> globalna konfiguracja naszej aplikacji.

Ruting

To w jaki sposób będzie się odbywać komunikacja między przeglądarką a aplikacją zależy właśnie od tego mechanizmu. Jest to swego rodzaju nakładka na vue-router. Każda strona powinna znaleźć się w folderze pages i do niej automatycznie zostanie stworzony ruting. Jeśli plik nazywa się about.vue adres to naszadomena/about. By tworzyć ruting dynamiczny (z podaniem dodatkowych parametrów np. id) nazwa pliku musi zawierać prefiks “_“. Dla przykładu pages/users/_id.vue zostanie przetłumaczony na np. naszadomena/users/1. Z kolei z poziomu strony linki tworzymy analogicznie do <nuxt-link to=”Nazwa rutingu”>Przyjazna nazwa</nuxt-link>.

API

Pracując z Nuxt.js musimy pamiętać o różnicy między renderowaniem po stronie serwera a renderowaniem po stronie klienta. Gdy jesteśmy na poziomie serwera (Node.js) nie mamy dostępu do takich obiektów jak window czy document, które są dostępne po stronie przeglądarki. Zatem gdy jest taka konieczność, część logiki możemy przenieść na klienta wykorzystując w tym celu metodę beforeMount() lub mounted(), tak jak poniżej:

beforeMount{
window.alert('hello');
}
mounted{
window.alert('hello');
}

Ogólnie rzecz ujmując w momencie gdy przeglądarka wysyła żądanie do serwera Nuxt.js generuje HTML i wykonuje metody asyncData(), nuxtServerInit() i fetch(). Następnie przeglądarka odbiera taką stronę i Vue.js zaczyna być uruchomiony. Po czym użytkownik wędruje po stronach używając komponentu całkowicie bez uderzania do serwera.

Różnica między asyncData() i fetch()

Na koniec muszę wspomnieć o moich problemach ze zrozumieniem kiedy wywoływać metodę asyncData() a kiedy fetch() z API. Różnica jest dosyć subtelna, nie mniej istotna. Metoda asyncData() jest wykonywana po stronie serwera przed renderowaniem, fetch() podobnie przy czym nie ustawia obiektów po pobraniu i dlatego lepiej go stosować wraz z Vuex.
Co ciekawe obie metody mogą być triggerowane gdy wymagają ich konkretne podstrony (tj. już po stronie klienta).

Tyle na dziś, więcej na
https://www.smashingmagazine.com/2020/04/getting-started-nuxt/
https://nuxtjs.org/

Przestrzenie nazw i moduły w ES6

O zaletach korzystania z przestrzeni nazw w takich językach jak C++, Java czy PHP chyba nie trzeba nikogo przekonywać. Właściwie w dużych projektach, zwłaszcza w takich co silnie wykorzystują zewnętrzne biblioteki odpowiednia separacja kodu jest niezbędna. W tym artykule będę chciał przedstawić jak się sprawy mają w JavaScript historycznie jak i w dobie standardu ECMAScript 6 (ES6).

Dla osób przyzwyczajonych do technologii C++, Java czy PHP może być pewnym szokiem, że przestrzenie nazw jako takie w JavaScript nie istnieją. Zamiast tego wykorzystuje się tu koncepcje modułów, chociażby za sprawą ES6, który je ustandaryzował. Przedtem obowiązywały standard AMD z jego implementacją require.js dla rozwiązań po stronie klienta oraz CommonJS dla środowiska Node.Js. Był jeszcze standard UMD, który próbował połączyć obydwa te rozwiązania. To co stanowiło siłę tych technologii to ograniczenie modułów do jednego pliku, automatycznie załączanie modułów na podstawie ich wzajemnych zależności czy możliwość wczytywania poszczególnych modułów z różnych bibliotek bez ładowania całości.

Przykład z http://papoldesign.pl/edukacja/requirejs-czym-amd-api-podstawy-uzycia-loadera/

requirejs.config({
paths: {
'jquery': 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min'
}
});

require(['jquery'], function ($) {

require(["one", "two", "three"], function (one, two, three) {
$('h1').html(three - one + two);
});
});

gdzie na początku ustalamy ścieżkę do biblioteki jQuery, następnie deklarujemy moduł wstrzykując jQuery w zmiennej “$” oraz dociągamy kolejne moduły (one.js, two.js ,three.js).

A jak to wygląda obecnie, w dobie ECMAScript 6?
Właściwie podobnie, moduły znajdują się w pliku js z tą różnicą, że jego wczytanie odbywa się przez słowo kluczowe import a deklaracja poprzez użycie export. Możemy eskportować zarówno funkcje, zmienne, stałe jak i klasy.

//plik pi.js
export let PI = () => {
return 3.14;
};

//plik main.js
import * as math from '~/static/pi.js'
console.log(math.PI());

To o czym warto pamiętać w kontekście modułów z ES6 to, że:

  • domyślnie stosują tryb strict,
  • są one wczytywane asynchronicznie,
  • zmienne lokalne widoczne są jedynie w obrębie modułu,
  • są wywoływane tylko raz,
  • można je też zadeklarować poprzez znacznik “<script type=module>“.

Pewnym problemem może by wczytywanie dynamicznie modułów, powszechnie stosowanie rozwiązanie sprowadza się w takim przypadku do wykorzystania async/await

sync function load() {
const obj = await import("./functions.js");
module.smallText();
module.bigText();
}

const button = document.querySelector("button");
button.addEventListener("click", async () => {
const obj = await import("./functions.js");
module.smallText();
module.bigText();
});

Muszę przyznać, że ES6 naprawdę wymiata. Więcej pod https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules

Dziedziczenie w JavaScript

Dzisiaj słów kilka o dziedziczeniu w JavaScript. Tym wpisem chciałbym zainaugurować serią artykułów na temat różnych smaczków tego języka. Mam nadzieje, że to mi się uda a i że wy odkryjecie w tym coś interesującego dla was samych.

JS posiada dziedziczenie oparte o prototypach, stanowi to odmienne podejście niż to znane nam z języków takich jak np. Java czy PHP. Główna różnica to na brak klas (chociaż standard ECMAScript 6 wprowadził je do języka to i tak „pod spodem” mamy do czynienia z prototypami. Co to takiego te prototypy? Upraszczając, to zdolność obiektu do posiadania referencji __proto__, która przechowuje właściwości i metody będące klonowane (skopiowane) do klasy potomnej. Każdy obiekt w JavaScipt zawiera taką referencje, jest to tak zwany obiekt nadrzędny. W momencie gdy “klasa” potomna nie posiada żądanej metody, to silnik JS sprawdza obiekt rodzica a następnie obiekt nadrzędny w celu uzyskania danej metody. Poniżej prosty przykład, który to obrazuje.

// konstruktor
function Book(name) {
this.name = name || 'Eloquent JavaScript';
};

//dodawanie do prototypu
Book.prototype.getName = function () {
  return this.name;
};

// utworzenie obiektu
var book = new Book();

// dziedziczenie
var technicalBook = Object.create(book); //Object.create istnieje od standardu ECMAScript 5

console.log(technicalBook.getName()); // Zwróci Eloquent JavaScript

Z kolei od ECMAScript 6 moglibyśmy to zapisać w taki sposób

class Book {
  setName(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

class TechnicalBook extends Book {

}

let technicalBook = new TechnicalBook();
technicalBook.setName('Eloquent JavaScript');

console.log(technicalBook.getName()); // Zwróci Eloquent JavaScript
console.log(technicalBook.name); // Zwróci Eloquent JavaScript

Tyle na dzisiaj, a wy którą składnie bardziej preferujecie?

Jak ułożyć diete na redukcję?

Dzisiejszy wpis będzie uzupełnieniem tematu, który poruszyłem jakiś czas temu tutaj. Spróbuję pokrótce opisać w jaki prosty sposób możemy ułożyć prostą dietę na redukcję. Zastrzegam przy tym, że prezentowane poniżej informacje są jedynie poglądowe. Jeśli jesteś zainteresowany ich wcieleniem w życie, to wcześniej skonsultuj się ze specjalistą.

Zanim przejdziemy do rozpisania konkretnego planu żywieniowego musimy odpowiedzieć sobie na kilka pytań. W pierwszej kolejności trzeba określić własny somatotyp (typ sylwetki). Pozwoli to nam lepiej dobrać zapotrzebowanie na makroskładniki. Wystarczy tu porównać prezentowane typy sylwetki z naszą. W tym przypadku precyzja nie jest zalecana bo w rzeczywistości spotyka się połączone w sobie cechy budowy różnych somatotypów. Nie mniej dla ektomorfików podstawą diety będą węglowodany, a dla endomorfików tłuszcze. Kolejny krok to określenie BMR, tj. ilość kilokalorii, które potrzebuje nasz organizm, by utrzymać wszystkie funkcje życiowe (pomijając aktywność fizyczną). W internecie można spotkać wiele kalkulatorów pozwalających nam określić BMR, jednak za radą z książki “Najpierw wiedza potem rzeźba” ja postanowiłem uwzględnić również beztłuszczową masę ciała (LBM). W dużym uproszczeniu właściwe zapotrzebowanie kaloryczne (uwzględniające aktywność fizyczną) wyliczymy według wzoru:

LBM × 24 × A (w przypadku mężczyzn)
LBM × 22 × A (w przypadku kobiet),

gdzie pod A podstawiamy
1,2 — znikoma aktywność fizyczna (np. praca za biurkiem).
1,3 — mała aktywność fizyczna (np. ćwiczenia raz w tygodniu).
1,4 — średnia aktywność fizyczna (np. dwa dość intensywne
treningi w tygodniu).
1,5 — duża aktywność fizyczna (np. kilka cięższych treningów
w tygodniu).
1,6 — bardzo duża aktywność fizyczna (minimum cztery cięż-
kie treningi w tygodniu lub praca fizyczna).

Tematem dzisiejszego wpisu jest stworzenie diety na redukcje dlatego teraz zmniejszymy zapotrzebowanie kaloryczne o 10 % (zaleca się pozostać w przedziale 10 – 20 %). Przykładowo 2160 − 10% × 2160 = 1944 kcal – tyle będzie trzeba dostarczyć dziennie organizmowi energii.

Istotną rolę w każdej diecie stanowi odpowiednie zbilansowanie makroskładników. Zaliczamy do nich węglowodany, białka i tłuszcze. Najwięcej węglowodanów znajdziemy w owocach i warzywach, kaszach, ryżu, ziemniakach i batatach. Z kolei białek w rybach, jajkach, nabiale (jogurty, sery wiejskie), natomiast tłuszcze w mięsie (np. boczku), awokado, tłuszczach zwierzęcych (np. smalec, masło). Proporcje między makroskładnikami w naszej diecie określimy na podstawie typu sylwetki. Dla mezomorfika będzie to 40% węglowodanów, 30% białek, 30 % tłuszczy, a dla endomorfika 25% węglowodanów, 35% białek i 40 % tłuszczy.

Tak uzbrojeni w wiedzę możemy przejść do obliczenia właściwej gramatury posiłków. By to osiągnąć należy tak dobrać składniki naszej diety by nie przekroczyć naszego dziennego zapotrzebowania na kalorie a jednocześnie uwzględnić właściwy podział na makroskładniki. Pomocna w tym celu będzie ta tabelka.

MakroskładnikKcal
1g węglowodanów4 kcal
1g białek4 kcal
1g tłuszczów9 kcal

W zależności od naszego typu sylwetki i dziennego zapotrzebowania na kalorie:

Węglowodany: 25% × 2000 kcal = 500 kcal (500 ÷ 4) tj. 125 g.
Białka: 35% × 2000 kcal = 700 kcal tj. 175 g
Tłuszcze: 40% × 2000 kcal = 800 kcal tj. 88 g.

Wszystko teraz sprowadza się do zbilansowania gramatury każdego posiłku w naszej diecie. Nie możemy przekroczyć wyliczonej wcześniej gramatury. By ułatwić jej ustalenie dla każdego posiłku w danym dniu, użyjemy wzoru:

Gramatura ÷ 100 × Makroskładnik na 100 g

Przykładowo ryż brązowy na 100 g posiada 23,9 g węglowodanów. Dla gramatury 50 g spożyjemy 12 g węglowodanów. Pozostaje tak dobrać posiłki i ich gramaturę by suma makroskładników zgadzała się z naszą dietą (polecam to zrobić w Ekselu).

Zastanówmy się jak mógłby wyglądać jeden przykładowy dzień. Na śniadanie jogurt naturalny (150 g) z musli, orzechami i suszonymi owocami (50 g). Drugie śniadanie to wołowina (100 g) z ryżem brązowym (30 g) podana z warzywami na patelnię. Na obiad risotto z kurczakiem (320 g). Podwieczorek sałatka z serem feta (125 g), a na kolacje kasza gryczana (50 g) z ogórkami kiszonymi. Mniam….

Reasumując, informacje tutaj zebrane są jedynie poglądowe. Jeśli zdecydujesz się na jakiś konkretny plan żywieniowy, to wcześniej skonsultuj to z dietetykiem. Nie ma też co traktować diety jako coś złego, najistotniejsze w tym wszystkim to czuć się z tym dobrze. Dlatego też nie ma sensu zanadto przejmować się tymi wszystkimi wyliczeniami i skrupulatnie się ich trzymać. Warto również przygotować sobie listę zakupów i posiadać potrzebne składniki “na zapas”, dzięki temu dużo łatwiej będzie nam wytrwać w naszych postanowieniach. Ja ze swojej strony kibicuje każdemu, kto dbając o własną sylwetkę sprawia, że jest bardziej efektywny 😉

MigrationMapper – sposób na mapowanie danych ze zmienną ich strukturą

Jedna z wspaniałych rzeczy w IT to ta możliwość w kółko podchodzenia do starych problemów tworząc nowe rozwiązania(unifikacje) jednocześnie podważając dotychczasowe dogmaty. Właśnie w takim duchu będzie dzisiejszy wpis, czyli moja próba połączenia antywzorca EAV (wyjaśniony niżej) z wzorcem Data Mapper. Moim celem jest utworzenie uniwersalnego mechanizmu do migracji danych z zewnętrznych serwisów. Opiszę to na podstawie aplikacji wspomagającej naukę języków obcych.

Na początku jednak kilka definicji użytych rozwiązań:

  • Data Mapper – “(…) to warstwa aplikacji odzielająca obiekty, którymi operujemy w pamięci, od bazy danych. Jej funkcją jest przekazywanie danych pomiędzy dwoma środowiskami przy jednoczesnym zachowaniu ich izolacji(…)“. M. Fowler.
  • Antywzorzec EAV (ang. Entity-Attribute-Value) – model danych, gdzie zarówno atrybuty jak ich wartości są zapisane w postaci rekordów, łamiąc tym samym pierwszą postać normalną.
  • Strategy“Definiuje rodzinę wymiennych algorytmów i kapsułkuje je w postaci klas. Umożliwia wymienne stosowanie każdego z nich w trakcie działania aplikacji niezależnie od korzystających z nich użytkowników”. Gang of Four.
  • Iterator “Zapewnia sekwencyjny dostęp do podobiektów zgrupowanych w większym obiekcie”. Gang of Four.
  • Dependency injection “Wzorzec polegający na usuwaniu bezpośrednich zależności pomiędzy komponentami (…) Jest to realizacja paradygmatu odwrócenia sterowania. M. Fowler
  • Repository – “Ogniwo łączące warstwę dziedziny oraz warstwę odwzorowania danych, wykorzystując przy tym interfejs przypominający kolekcję, który zapewnia dostęp do obiektów warstwy dziedziny“. M. Fowler.

Główną przesłanką za stosowaniem wzorca Data Mapper to wprowadzenie pewnej izolacji między bazą danych a aplikacją. Taka izolacja uprości implementacje z użyciem wzorców Lazy Load, Identity Map, Register czy Repository po stronie logiki dziedziny. Natomiast po stronie bazy danych użyty antywzorzec EAV zapewni sporą elastyczność dla dalszych rozszerzeń. Model danych składa się tutaj z trzech tabel:

  • Migration (tabela encji)
  • MigrationData (tabela atrybutów)
  • konkretna tabela (tabela wartości) np. w moim projekcie DifficultWord, StatsCourse

Założenie jest następujące. Wszystkie serwisy zdolne przeprowadzić migracje danych z zewnętrznego serwisu są przechowywane w tabeli Migration. Uzyskane dane trafiają do konkretnych tabel tutaj np. DifficultWord (tabela wartości). Natomiast informacje (metadane) o sposobie mapowania pobranych danych są zapisane jako atrybuty – w MigrationData. Zapisuje się tu również czas ostatniej migracji danych, tworząc niejako historię tych migracji. Tym samym prowadzi to do ich redundancji (co jak wspomniałem jest skutkiem złamania pierwszej postaci normalnej). Jest to jednak w tym przypadku pożądany efekt.

Przejdźmy teraz do samej aplikacji. W celu wydobycia danych np. z tabeli DifficultWord należy rozszerzyć klasę AbstractMapper o metodę zwracającą kolekcje obiektów konkretnej encji tutaj DifficultWord. Taka kolekcja musi wspierać iteracje (wzorzec iterator). Elementem opcjonalnym jest tutaj sposób / strategia pobierania identyfikatorów (kluczy do mapowania). Klasa pochodna może wymusić wstrzyknięcie innej strategi wyciągania kluczy. W moim przypadku taka potrzeba zaszła dla klasy DifficultWordPerWeekMapper, która wyciąga obiekty z bazy w odmienny sposób niż domyślna strategia, grupując je względem tygodni. Poniżej przykład.

    /**
     * @return Collection
     * @throws DBALException
     */
    public function getLocalIds(): Collection
    {
        $sql = <<<SQL
SELECT numberWeek, d.local_id as localId
FROM migration as m
LEFT JOIN migration_data as d ON m.id = d.migration_id,
     (
         SELECT strftime('%W', m.create_at) as numberWeek, max(m.id) 
         as migrationId
         FROM migration as m
         WHERE status = 1 AND `type` = :type
         GROUP BY numberWeek
     )
WHERE m.id = migrationId

SQL;

        /** @var Statement $query */
        $query = $this->doctrine->getConnection()->prepare($sql);
        $query->bindParam(':type', $this->type);
        $query->execute();

        return new ArrayCollection($query->fetchAll());
    }

Gdy już posiadamy identyfikatory właściwych rekordów do pobrania, konkretna realizacja klasy AbstractMapper dostarcza logikę pobrania danych w metodzie getData() używając w tym celu repozytorium. Przykład dla DifficultWord poniżej.

 public function getData(): Collection
    {
        $difficultWordIds = $this->getLocalIds()->getValues();

        return new ArrayCollection($this->difficultWordRepository
          ->findBy([
            'id' => $difficultWordIds
        ]));
    }

Prezentowane rozwiązanie spełnia wszystkie postawione przeze mnie kryteria. Kod i jego architektura jest prosta, czytelna i łatwo rozszerzalna. Dla kolejnych bytów w aplikacji wystarczy dopisać odpowiedni maper, a w skrajnych sytuacjach również strategię pobierania kluczy. Całość całkiem dobrze współgra z wspomnianymi wzorcami Lazy Load, Identity Map, Register, Repository czy Iterator. Jednakże takie rozwiązanie wykorzystujące antywzorzec EAV może w przyszłości stworzyć wiele problemów: “(…) Musimy jednak pamiętać, że doświadczeni konsultanci zajmujący się bazami danych konsekwentnie powtarzają, że systemy stosujące ten antywzorzec (tutaj EAV) tracą sprawność już w ciągu roku od wdrożenia.Bill Karwin, Antywzorce języka SQL. Na jeszcze inną kwestie zwraca uwagę Fowler opisując konsekwencje używania Data Mapper a tyczy się ona problemów wydajnościowych. Data Mapper niekiedy by wyciągnąć jeden obiekt musi kilkukrotnie odpytać bazę danych. W najprostszym przypadku aplikacja dla każdego żądania generuje po dwa zapytania do bazy danych. Przy czym liczba kolejnych żądań może się zwiększyć z czasem. Wydaje się jednak, że dla prostych projektów bez zbytnio skomplikowanej logiki dziedziny, taki sposób obsługi danych podlegających migracji powinien wystarczyć.

Mój Jenkinsfile pod Symfony / PHP

O zaletach stosowania i czym w ogóle jest Continuous integration(CI) nie będę się w tym wpisie rozwodził. Wydaje się, że tyle już zostało powiedziane w tym temacie, że to wszystko staje się aż do bólu oczywiste w samej teorii. Dlatego dzisiejszy wpis chce by miał bardziej praktyczne podejście do tego. Od dłuższego czasu szukam jakiejś w miarę sensownej konfiguracji Jenkinsa pod projekty pisane w PHP. Niestety od momentu wprowadzenia Pipeline, wiele rozszerzeń dedykowanych pod PHP po prostu z tym nie współgra. Poniżej prezentuje co udało mi się ustalić i jak to wygląda w moim projekcie gdzie na pokładzie jest Symfony4 z testami pisanym w PHPUnit. Jednakże dla osób niezaznajomionych  lub wciąż głodnych dodatkowych informacji, z całego serca mogę polecić książkę Continuous Integration: Improving Software Quality and Reducing Risk.

Na świeżej instalacji Jenkinsa 2.176.1 musiałem dodatkowo zainstalować następujące pluginy:

  • PHP Built-in Web Server
  • HTML Publisher – umożliwia publikowanie raportu z pokrycia kodu testami
  • Clover – ustawia zachowania build’a na podstawie wyników z pokrycia kodu
  • Clover PHP – PHP’owy wrapper na Clover
  • Javadoc – prezentuje dokumentacje z phpdoc
  • Static Analysis Utilities
  • Checkstyle – przechwytuje raport z phpcs
  • PMD – przechwytuje raport z phpmd
  • DRY – przechwytuje raport z phpcpd
  • Bitbucket Plugin – umożliwia przechwycenie webhook’a z Bitbucket (więcej o tym w dalszej części wpisu)

Poniżej Jenkinsfile

#!/usr/bin/env groovy

node {
    
    stage('Get code from SCM') {
        checkout(
                [$class: 'GitSCM', branches: [[name: 'master']],
                 doGenerateSubmoduleConfigurations: false,
                 extensions: [],
                 submoduleCfg: [],
                 userRemoteConfigs: [[url: 'git@adresrDoRepo.git',  credentialsId: 'user']]]
        )
    }

    stage('Prepare') {
        sh 'composer install'
        sh 'bin/console assets:install'
        sh 'bin/console cache:clear'
        sh 'bin/console doctrine:database:create'
        sh 'bin/console doctrine:migrations:migrate --no-interaction'
    }

    stage('PHP Syntax check') { 
        sh 'vendor/bin/parallel-lint --exclude vendor/ --exclude ./bin .'
    }
    
    stage('Symfony Lint') {
        sh 'bin/console lint:yaml src'
        sh 'bin/console lint:yaml tests'
        sh 'bin/console lint:twig src'
        sh 'bin/console lint:twig tests'
    }

    stage("PHPUnit") {
        sh 'bin/phpunit --coverage-html build/coverage --coverage-clover build/coverage/index.xml'
    }

    stage("Publish Coverage") {
        publishHTML (target: [
                allowMissing: false,
                alwaysLinkToLastBuild: false,
                keepAll: true,
                reportDir: 'build/coverage',
                reportFiles: 'index.html',
                reportName: "Coverage Report"

        ])
    }

    stage("Publish Clover") {
        step([
            $class: 'CloverPublisher',
            cloverReportDir: 'build/coverage',
            cloverReportFileName: 'index.xml',
            healthyTarget: [methodCoverage: 70, conditionalCoverage: 80, statementCoverage: 80], // optional, default is: method=70, conditional=80, statement=80
            unhealthyTarget: [methodCoverage: 50, conditionalCoverage: 50, statementCoverage: 50], // optional, default is none
            failingTarget: [methodCoverage: 0, conditionalCoverage: 0, statementCoverage: 0]     // optional, default is none
        ])
    }

    stage('Checkstyle Report') {
        sh 'vendor/bin/phpcs --report=checkstyle --report-file=build/logs/checkstyle.xml --standard=phpcs.xml --extensions=php,inc -wp || exit 0'
        checkstyle pattern: 'build/logs/checkstyle.xml'
    }

    stage('Mess Detection Report') {
        sh 'vendor/bin/phpmd . xml phpmd.xml --reportfile build/logs/pmd.xml || exit 0'
        pmd canRunOnFailed: true, pattern: 'build/logs/pmd.xml'
    }

    stage('CPD Report') {
        sh 'vendor/bin/phpcpd --log-pmd build/logs/pmd-cpd.xml --exclude bin --exclude vendor --exclude src/Migrations --exclude var . --progress || exit 0'
        dry canRunOnFailed: true, pattern: 'build/logs/pmd-cpd.xml'
    }

    stage('Lines of Code') {
        sh ' vendor/bin/phploc --count-tests --log-csv build/logs/phploc.csv --log-xml build/logs/phploc.xml . --exclude vendor --exclude src/Migrations --exclude var .'
    }

    stage('Software metrics') {
        sh 'vendor/bin/pdepend --jdepend-xml=build/logs/jdepend.xml --jdepend-chart=build/dependencies.svg --overview-pyramid=build/overview-pyramid.svg --ignore=vendor,var,bin,build .'
    }

    stage('Generate documentation') {
        sh 'vendor/bin/phpdox -f phpdox.xml'
    }
    stage('Publish Documentation') {
        publishHTML (target: [
                allowMissing: false,
                alwaysLinkToLastBuild: false,
                keepAll: true,
                reportDir: 'docs/html',
                reportFiles: 'index.xhtml',
                reportName: "PHPDox Documentation"

        ])
    }

}

Wycinek z composer.json

"require-dev": {
        "jakub-onderka/php-parallel-lint": "^1.0",
        "pdepend/pdepend": "@stable",
        "phploc/phploc": "^5.0",
        "phpmd/phpmd": "@stable",
        "sebastian/phpcpd": "^4.1",
        "squizlabs/php_codesniffer": "*",
        "symfony/debug-pack": "*",
        "symfony/maker-bundle": "^1.0",
        "symfony/phpunit-bridge": "^5.0",
        "symfony/profiler-pack": "*",
        "symfony/test-pack": "*",
        "symfony/web-server-bundle": "4.3.*",
        "theseer/phpdox": "^0.12.0"
    },

A tak wygląda mój dashboard po tych buildach

W trakcie implementacji natknąłem się na dwa problemy. Pierwszy dotyczył ustawienia w Jenkinsie Credentials dla wygenerowanego klucza z dostępem do repozytorium Git. Pamiętajmy, że trzeba taki klucz publiczny utworzyć i zapisać w ustawieniach Jenkinsa – Credentials. Następnie w pliku Jenkinsfile w parametrze userRemoteConfigs należy ustawić credentialsId z identyfikatorem podanym w zakładce Credentials. Ciekawą opcją w przypadku używania akurat Bitbucket jest możliwość wygenerowania klucza z dostępem tylko do odczytu repozytorium. Zdecydowanie polecam tą opcję dla przyznania dostępu dla Jenkinsa. Inną problematyczną kwestią z jaką się spotkałem było przechwycenie webhooka z Bitbucket. Albo występował problem z uprawnieniami, albo serwer Jenkinsa klasyfikował żądanie jako nieprawidłowe. Gdy się z tym uporałem okazało się, że build dla gałęzi master idzie także wtedy gdy zostanie zrobiony push do innego brancha. Koszmar! Rozwiązanie, użyć pluginu bitbucket w Jenkinsie.

A Wy jaką macie konfiguracje ? 😉

Kiedy aplikacja nie udostępnia czytelnego API

Cześć, ostatnio pracując nad nowym projektem w PHP zderzyłem się z koniecznością integracji mojej aplikacji z zewnętrzną usługą. Niestety owa usługa nie udostępnia żadnego sensownego interfejsu ala REST. Dlatego też musiałem się trochę napocić by otrzymać interesujące mnie dane. Dzisiejszy wpis będzie o tych zmaganiach i wykorzystaniu w tym celu bibliotek CURL (dla połączenia), fabpot/goutte (do swobodnego przechodzenia po drzewie DOM) i kegi/netscape-cookie-file-handler (do manipulacją plikami cookie lokalnie).

Zanim zaczniemy, uwaga na CSRF

By mógł otrzymywać interesujące mnie informacje, muszę się wcześniej zalogować do aplikacji. Pierwszym wymaganym krokiem jest otrzymanie tokenu ze zdalnej witryny, który służy zabezpieczeniu przed atakiem CSRF. Przykładowy kod wykorzystujący bibliotekę fabpot/goutte prezentuje poniżej.

private function getCsrfToken(): string
    {
        $crawler = $this->client->request('GET', self::SERVICE_LOGIN_URL);
        $token = $crawler->filter('input[name=csrftoken]')->attr('value');
        return $token;
    }

gdzie SERVICE_LOGIN_URL zawiera adres URL do zdalnej witryny, która z kolei zwraca swój token w inpucie o nazwie csrftoken. Tak przechwycony token będziemy musieli później odsyłać do tej witryny (zgodnie z jej protokołem).

Uwierzytelnienie

Mając pobrany token, możemy przystąpić do logowania. Oczywiście będziemy musieli posiadać login i hasło. Sposób logowania zależy od konkretnej witryny, w moim przypadku wygląda to tak.

private function auth(string $token): string
    {
        $postdata = http_build_query(
            array(
                'csrftoken' => $token,
                'username' => $this->login,
                'password' => $this->password,
            )
        );

        $headerArray = [
            'Content-Type: application/x-www-form-urlencoded',
            'referer: http://' . self::SERVICE_LOGIN_URL,
            'Accept: */*',
            "Cookie: csrftoken=$token;",
        ];

        $ckfile = tempnam(self::COOKIE_DIR, "CURLCOOKIE");
        try {
            $ch = curl_init(self::SERVICE_LOGIN_URL);
            curl_setopt($ch, CURLOPT_COOKIEJAR, $ckfile);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headerArray);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
            curl_setopt($ch, CURLOPT_COOKIEFILE, $ckfile);
            curl_setopt($ch, CURLOPT_VERBOSE, true);
            $output = curl_exec($ch);

            if ($output === false) {
                throw new \Exception(sprintf('No connection with service, msg: %s', curl_error($ch)));
            }
            $filenameArray = explode(DIRECTORY_SEPARATOR, $ckfile);
            return end($filenameArray);

        } finally {
            curl_close($ch);
        }
    }

Zatem token, login i hasło zostaną wysłane metodą POST pod URL ze stałej SERVICE_LOGIN_URL (opcja CURLOPT_POST z danymi z klucza CURLOPT_POSTFIELDS ). Do poprawnego obsłużenia żądania konieczne jest wysyłanie dodatkowo kilku nagłówków w tym cookie zawierającego token (CURLOPT_HTTPHEADER). Opcjonalne pozostaje użycie CURLOPT_RETURNTRANSFER i CURLOPT_VERBOSE – ułatwiają śledzenie błędów.

Serwer w przypadku poprawnego uwierzytelnienia wyśle dane (identyfikator sesji) do pliku cookie. Stąd konieczne jest jego wcześniejsze utworzenie przy użyciu funkcji tempnam() generującej losowo nazwany plik. Następnie odczyt tego pliku ułatwia biblioteka kegi/netscape-cookie-file-handler, jak poniżej.

 private function saveSessionId(string $ckfile): string
    {
        $configuration = (new Configuration())->setCookieDir(self::COOKIE_DIR);
        $cookieJar = (new CookieFileHandler($configuration))->parseFile($ckfile);
        $sessionIdObject = $cookieJar->get('sessionid');
        $sessionId = $sessionIdObject->getValue();
        $expire = $sessionIdObject->getExpire();
...

W dalszym kroku identyfikator sesji zostanie zapisany ze zmiennej $sessionId do bazy danych, tak by mógł zostać wykorzystywany dla późniejszych requestów.

Przykładowy request

Wreszcie przyszedł czas na request, który pozwoli nam na wykonanie jakiejś akcji po stronie zewnętrznej witryny. Może to wyglądać na przykład tak jak poniżej (pobranie danych do zmiennej $table zawartych w klasie .table drzewa DOM)

$crawler = $this->client->request('GET', self::SERVICE_SOME_TABLE_URL);
$table = $crawler->filter('.table')->first();

ufff….mimo to, jednak wciąż wolę REST’a 😉

Zdalna Fasada

Jako zwolennik leniwej optymalizacji, czyli nie przejmowania się wydajnością aplikacji dopóki owa aplikacja nie zacznie spowalniać – mam kłopot z dzisiejszym wpisem. Jest tak, ponieważ chciałbym tutaj naświetlić wzorzec projektowy Remote FacadeZdalna Fasada, który wymusza na nas taką optymalizacje już na etapie projektu architektury programu. Wpis ten jest rozwinięcie tematu o Data Transfer Object, a oba wzorce doskonale się uzupełniają.

Problem jaki przeważnie niesie optymalizacja to trudne do pogodzenia światy: czytelności kodu oraz jego wydajności. Stąd jest ważne by oprzeć poprawę efektywności kodu na jakimś sprawdzonym wzorcu. Zacznijmy jednak od początku, czym jest Remote Facade?

“Zapewnia ogólną fasadę dla szczegółowych obiektów, której celem jest poprawa efektywności ruchu sieciowego.”

gdzie przez “ogólną fasadę” mamy tu namyśli wzorzec Fasady z Gangu Czworga. Taki obiekt upraszcza nam interfejs, a w przypadku obiektów zdalnych ma za zadanie zmniejszyć ilość interakcji między nimi. Zdalnym obiektem może tu być nie tylko obiekt wywoływany na przeciwnym krańcu kuli ziemskiej, ale nawet obiekt wywoływany na tym samym komputerze (w ramach innego procesu).

Remote Facacde zastępuje wszystkie metody set() i get() jedną metodą set oraz jedną metodą get (zwanymi jako blokowe metody dostępu). Fasada może działać na zasadzie bramy dostępowej. Implementacja nie powinna być trudna , ważne jest tutaj by wybrać taki format przechowywania danych, aby łatwo można było te dane wysłać przez sieć. Trzeba uważać na serializacje całego obiektu, nie zawsze jest to dobry pomysł. W takim przypadku warto użyć DTO. Innym problemem wynikającym z stosowania Remote Facade jest poziom szczegółowości. Możemy mieć wiele fasad obejmujących stosunkowo niewielki zakres działania lub wprost przeciwnie ograniczyć się jedynie do kilku. To drugie podejście zaleca Fowler w swojej książce Architektura Systemów Zarządzania Przedsiębiorstwem. Wzorce projektów. Ciekawym pomysłem jest wykorzystanie Remote Facade jako mechanizmu zabezpieczeń czy obsługi transakcji. Listy dostępu mogą określać, który użytkownik ma dostęp do wywołania danej metody.

Na zakończenie, warto mieć na uwadze, że Remote Facade nie może zawierać żadnej logiki dziedziny. Jeśli konieczne jest użycie dziedziny np. do sterowania przebiegiem pracy czy koordynowania to można albo implementacje przenieść do obiektów szczegółowych lub użyć wzorca Transaction Script. Remote Facade nie użyjemy wszędzie tam gdzie potrzebujemy asynchronicznych komunikatów, zresztą takie podejście mija się z celem stosowania fasady.

Więcej informacji na ten temat w książce “Architektura Systemów Zarządzania Przedsiębiorstwem. Wzorce projektowe“. M. Fowler.

Parametryzowanie Dockerfile

Trudno sobie dzisiaj wyobrazić rozwój dużego projektu bez wykorzystywania dobrodziejstw jakie daje nam wirtualizacja. Po prostu ilość potrzebnych komponentów, relacji między nimi, całego procesu developmentu, orkiestracji jest tak olbrzymia i skomplikowana, że bez Dockera nie dalibyśmy rady.

Jednakże narzędzia takie jak docker i docker-compose nie są panaceum na wszystkie problemy. Ostatnio stanąłem przed wyzwaniem rozdzielenia logiki w trakcie budowy obrazu w pliku Dockerfile. Generalnie, potrzebna jest inna wersja na produkcji a inna w trybie deweloperskim.

Jako programista, głównie piszący w świecie obiektowym postanowiłem rozwiązać ten problem używając dziedziczenia. Jakże było to wielkie zaskoczenie dla mnie, gdy okazało się, że pliki Dockerfile nie posiadają takiego mechanizmu. Dziedziczeniu może podlegać jedynie logika zawarta w plikach docker-compose.yml…cóż, trzeba kombinować inaczej.

Rozwiązanie jakie udało mi się zaimplementować używa parametrów na etapie budowy obrazu – komenda arg [1][2]. Wygląda to następująco.

W głównym pliku budowy obrazu Dockerfile tworzymy zmienną (owy parametr) i przypisujemy domyślną wartość. Domyślna wartość może odpowiadać zachowaniu w np. trybie produkcyjnym.

ARG some_variable_name=default_value

W dalszej części pliku Dockerfile wykorzystujemy zmienną dostarczając zachowanie w zależności od wartości tego parametru. Właściwie jest to pewnego rodzaju flaga dla nas. Przykład:

RUN apt-get install $some_variable_name

Równie dobrze, można też [3]:

RUN if [ "x$some_variable_name" = "htop" ] ; then something action ; else echo other action ; fi

Pozostaje jeszcze nadpisać domyślną wartość np. dla trybu deweloperskiego. Poniżej zawartość docker-compose-dev.yml

services:
  someImage:
    build:
      context: ./
      dockerfile: Dockerfile
      args:
        some_variable_name: "htop"

Jeszcze innym rozwiązaniem tego problemu jaki się nasuwa to skonfigurowanie domyślnego obrazu po jego budowie i ponowne zapisanie stanu przy użyciu commit [4]. Ale jakoś to takie mało eleganckie posunięcie :/

[1] https://docs.docker.com/engine/reference/builder/#arg
[2] https://vsupalov.com/docker-arg-env-variable-guide/
[3] https://stackoverflow.com/questions/43654656/dockerfile-if-else-condition-with-external-arguments
[4] https://forums.docker.com/t/docker-container-not-persisting-data-on-stop-start-or-when-committing-to-another-image-on-windows/5612

Kod efektywny czy efektowny ? Alternatywa dla unset() w PHP

Jakiś czas temu zostałem zmuszony do skasowania pewnego elementu w tablicy, który oczywiście musiał być tak głęboko ukryty w całej tej hierarchii bym łatwo i przyjemnie nie mógł tego zrobić. W efekcie końcowym uzyskałem kod, który w pewnym sensie narusza prawo demeter[1], a symptomy są podobne do opisanego przez Wujka Boba problemu z wrakami pociągów (ang. Train Wrecks) [2]. O ile takie porównanie ma jakiś sens w kontekście operowania tablicami w PHP. Tak czy siak, być może poniższy kod jest efektywny, ale czy jest on efektowny ?

unset($carsArray['accessories'][0]['cockpit'][0]['mirror'])

Tak właściwie jest to częsta przypadłość języków dynamicznie typowanych. Postanowiłem przemyśleć sprawę raz jeszcze. Czy aby na pewno nie można by zrefaktoryzować tego kodu tak, by wyglądał choć odrobinę lepiej?

Struktury zmienić nie mogę, natomiast tą część logiki odpowiedzialnej za wyszukanie odpowiedniego klucza i jego skasowanie już tak. Stąd pierwszym pomysłem jaki mi wpadł do głowy to wykorzystanie pętli foreach na wielu poziomach, gdzie każda klasa znałaby strukturę tablicy wyłącznie dla swojego poziomu (zgodnie z prawem demeter). Można by też to oprzeć o kompozyt[3], ale gdybyśmy byli w świecie obiektów. Niestety funkcja unset() nie zadziała dla referencji uzyskanej z pętli foreach. Dlatego też rozwiązania musiałem szukać w wbudowanych w PHP funkcjach na tablicach [4]. W szczególności array_filter() [5].

class MirrorCleaner
{
    private $carsArray = [];

    public function __construct(array $carsArray)
    {
        $this->carsArray = $carsArray;
    }

    public function cleanMirror()
    {
        $newCarsArray = [];

        foreach ($this->carsArray as $value) {
            $accessoriesArray = $value['accessories'] ?? [];
            $newCarsArray[]['accessories'] = $this->parserAccessories($accessoriesArray);
        }

        return $newCarsArray;
    }

    private function parserAccessories(array $accessoriesArray)
    {
        $newAccessoriesArray = [];

        foreach ($accessoriesArray as $value) {
            $cockpitsArray = $value['cockpit'] ?? [];
            $newAccessoriesArray[]['cockpit'] = $this->parserCockpits($cockpitsArray);
        }

        return $newAccessoriesArray;
    }

    private function parserCockpits(array $cockpitsArray)
    {
        $newCockpitsArray = [];

        foreach ($cockpitsArray as $element) {
            $newCockpitsArray[] = $this->removeMirror($element);
        }

        return $newCockpitsArray;
    }

    private function removeMirror(array $value)
    {
        return array_filter($value, function ($key) {
            return !($key == 'mirror');
        }, ARRAY_FILTER_USE_KEY);
    }
}

Pełny kod na https://github.com/domino91/ArrayFilter

Nie jest to rozwiązanie idealne. Klasa MirrorCleaner narusza zasadę pojedynczej odpowiedzialności (parsuje tablice, kasuje elementy). Nadal złamane jest prawo demeter (klasa zna algorytm przetwarzania całej struktury). Nie jest to też rozwiązanie optymalne dla naprawdę sporych tablic czy aplikacji, w których czas wykonania ma kluczowe znaczenie (kopiujemy tu tablice). Pewnym krokiem naprzód byłoby zrefaktoryzowanie tej klasy o wydzielenie na kilka klas kompozytów i wstrzykiwanie do nich zależności…Mimo to jednak uważam obecne rozwiązanie za całkiem efektowne 😉 Co Wy o tym sądzicie ?

Źródło:
[1] https://www.tripled.io/25/08/2016/The-anemic-domain-model/
[2] https://hackerchick.com/clean-code/
[3] https://www.geeksforgeeks.org/composite-design-pattern/
[4] https://carlalexander.ca/php-array-functions-instead-loops/
[5] https://www.php.net/manual/en/function.array-filter.php