Sobakowy Blog 2018

Witaj, 2018 roku!

Nie planowałem co prawda rozpoczynać sezonu blogowego w połowie sierpnia, ale szczęśliwie mogę tym razem odezwać się z czymś więcej niż zwyczajowym "No hej, nie było mnie rok, teraz chyba będę, wpisy nadchodzą — kiedyś…". Udało mi się nareszcie skończyć punkt pierwszy, sam szczyt moich list todo od niemal roku. Z lekką dumą informuję, że znajdujesz się teraz na nowej, odświeżonej wersji mojej strony.

Prawdopodobnie… nie zauważyłeś różnicy. I nie są to problemy z cache przeglądarki, a zupełnie zamierzony zabieg. Uznałem, że obecna, czteroletnia forma strony jest w dalszym ciągu adekwatna i (szczególnie przy moich marnych umiejętnościach graficznych) nie ma się co silić na rewolucję. Zrobiłem więc ewolucję, która na drugi rzut oka manifestuje się w najróżniejsze sposoby.

Żegnaj, WordPressie!

Po pierwsze, po ponad ośmiu latach i po raz pierwszy od tchnięcia życia w sobak.pl pożegnałem się z WordPressem. Wspaniałe to było osiem lat, nie zapomnę ich nigdy. Poważnie mówiąc, nie było źle. Nie potrafię wskazać jednego momentu przełomowego, przelania się czary goryczy, które spowodowałoby że rozżalony powiedziałbym dość. Co więc zadecydowało? Suma trzech czynników:

  • Dawka zdrowej ambicji. Nie wypada szewcowi chodzić w najtańśzych butach z sieciówki i nie wypada programiście technologii webowych tworzyć swojej wizytówki na pierwszym lepszym darmowym systemie.

  • Obawy odnośnie bezpieczeństwa. Nie zamierzam tu powielać szeroko rozpowszechnionych mitów. Rdzeń WordPressa jest bezpieczny, pomimo kilku istotnych wtop z zakresu procedowania luk bezpieczeństwa, WordPress w swojej bazowej formie skutechnie opiera się większości ataków.

    To, co można powiedzieć o samym gołym produkcie, nie do końca jest jednak prawdą w wypadku niezliczonych wtyczek. Tak więc w obliczu migracji na VPS-a (o której słów kika za chwilę) poszukiwałem czegoś, co nie wprowadzi mnie na pole minowe.

  • Potrzeba customizacji. Raz jeszcze przyznać trzeba, że WordPress generalnie rozszerzalny jest. Twierdzenie inaczej byłoby zanegowaniem faktu istnienia tak wielu wtyczek i motywów do niego.

    Nie oznacza to jednak, że owe rozszerzanie należy w roku 2018 do przyjemnych. Mimo, że sam jestem autorem kilku z funkcjonalności integrujących się dosyć mocno w poprzednią wersję witryny, to tworzenie tychże można porównać do szambonurkowania. Wraz z rosnącą ilością zmian, które chciałem i chciałbym wreszcie wprowadzić na swojej stronie potrzebowałem czegoś, w czym czułbym się absolutnie swobodnie.

Dlatego też zdecydowałem się na stworzenie własnego backendu w oparciu o framework Laravel, roboczo określonego nazwą kodową Perception (podziękowania dla winka, jak zawsze niezastąpionego w spontanicznym wymyślaniu nazw). Teoretycznie jest to prosta aplikacja CRUD-owa, więc nie będę za wiele rozpisywał się na temat szczegółów implementacyjnych. Chcę jedynie wspomnieć o jednej ważnej decyzji powiązanej ze wspomnianym wyżej poszukiwaniem bezpieczeństwa. Z tego powodu, a także chęci uproszczenia sobie życia — zarówno w momencie developowania jak i korzystania z platformy — zdecydowałem się na zupełny brak panelu administracyjnego.

Obecna implementacja czerpie mocno z konceptu generatorów stron statycznych. Źródłem jej danych są bowiem statyczne pliki tekstowe. W przeciwieństwie jednak do wcześniej wymienionych generatorów nie są z nich tworzone gotowe pliki HTML, a jedynie przejściowa, tymczasowa baza danych, która pozwala na wprowadzenie dowolnej ilości dynamiki i w zasadzie klayczny rozwój aplikacji WWW. Jak wspomniałem jest to też wygodniejsze dla mnie jako użytkownika, bo oznacza że nie muszę walczyć z dowolnym skryptem, próbując do niego dodać wsparcie Markdowna czy używać desktopowego edytora tylko po to, aby później wkleić wynik swojej pracy do formularza na stronie.

Harder, Better, Faster, Stronger

Dużo uwagi poświęciłem poprawie wydajności działania strony. Począwszy od przeniesienia się na szybszy serwer, przez optymalizację backendu, aż po upewnienie się, że wynik jego pracy może zostać szybko pobrany i obsłużony przez przeglądarkę. Zacząłem od podstaw, czyli zmniejszenia ilości i rozmiaru tekstu wysyłanego do klienta, wagi obrazków, zapytań do innych zasobów et ceterra. Wynik całej tej operacji przedstawia skrótowo poniższa galeria.

Pimp my website

Mimo iż nie zdecydowałem się na całościową wymianę szablonu strony, znalazłem dziesiątki, jeśli nie setki pomniejszych rzeczy zasługujących na poprawę w obecnym. Poczynając od rzeczy ogólnych jak zwiększenie kontrastu tekstu, o które prosiliście niedługo po premierze poprzedniej wersji (4 lata, polecam się), przez poprawę typografii, marginesów, kolorów, spójności i podrasowanie pojedynczych podstron, jak portfolio. Więcej szczegółów we wmiaręwykazie zmian na dole wpisu.

Content is an old, impotent king

Wydaje mi się, że blog z założenia jest formą treści, która po czasie zaczyna stanowić pewną historię, więc nie ingerowałem za bardzo w samo sedno byłych wpisów. Nie da się jednak ukryć że przez ponad osiem lat wiele z nich zaczęło odstawać od reszty strony, nie tylko stylistycznie lecz nawet w tak jaskrawe sposoby jak użycie już dawno nieużywanej wtyczki do podświetlania kodu. Poprawiłem też mnóstwo znalezionych po drodze literówek, niedziałających już dawno linków, a czasem nawet skrajnie nieaktualnych lub błędnych informacji (to ostatnie jednak robiłem w drodze wyjątku).

W kontekście treści warto też wspomnieć o porzuceniu możliwości komentowania na mojej stronie. Nad trudnościami technicznymi przeważyły problemy z moderowaniem takiej ilości tekstu i późniejszym utrzymaniu go w ryzach. Wierzę że komentujący mi wybaczą i z formularza na dole wpisów zapraszam do kontaktu w dowolnym miejscu wykazanym w zielonym menu (ikona serduszka na prawo od nagłówka).

Projekty stare i nowe

Sporo aktualizacji otrzymało portfolio. Poza poprawkami stylów, drobnymi korektami wielu z miniaturek, skupiłem się także na aktualizacji zawartej tam treści. Rozbudowałem opisy wielu z projetków, te które doczekały się nowych wersji zyskały często osobne podstrony w portfolio, aby oddzielić je od historycznych już tworów. Wreszcie, opisałem także kilka nowych dzieł.

Pastebin, czyli nowe miejsce do dzielenia się fragmentami kodu źródłowego, które w obecnej formie powstało kilka nocy temu.

Blackoutopus magnum spierdolenia szczyt projektów bezsensownych, nieuzasadnionych i z góry skazanych na porażkę, czyli moje starcie z opisywaną już bestią, bossem projektów legacy — xNovą.

Codice, które od ostatniej aktualizacji pozycji w portfolio stało się zupełnie nowym, otwartoźródłowym projektem. Dawniejsza wersja i delikatny rys historyczny poprzednich podejść został przeniesiony na osobną podstronę.

I wreszcie zupełnie nowa odsłona sobak.pl w portfolio. Podobnie jak wyżej, dawna treść została zachowana w osobnym, podlinkowanym miejscu.

Co dalej?

W chwili gdy to piszę, z mojego osobistego todo znika najbardziej rozbudowany i pożądany punkt od niemal roku. Mogąc się teraz zabrać za inne rzeczy, bez jednoczesnego wywoływania wyrzutów sumienia, planuję okazać trochę miłości starym, zakurzonym projektom, jak i zająć się rozwojem kilku nowszych. Stay tuned!

…ach, nie spodziewajcie się jednak w najbliższym czasie nowych tworów wielkoformatowych pokroju Codice czy codziennych aktualizacji o postępie prac. Stety-niestety gimnazjum mam już za sobą, a programowanie zawodowe skutecznie ogranicza ilość sił i zwyczajną ochotę na dalsze zagłębianie się w kod w czasie wolnym. Postaram się jednak aby każdy miesiąc przyniósł jakąś nową informację o mnie i tym nad czym pracuję.

W-miarę-wykaz zmian

  • znacząco poprawiłem czas ładowania strony (porównanie wyżej)
  • wdrożyłem certyfikat SSL dla sobak.pl i wszystkich subdomen
  • gorsza wiadomość: ze względu na ograniczony czas wycofałem możliwość komentowania wpisów
  • oparłem podświetlanie bloków kodu źródłowego o Keylightera
  • przerobiłem podgląd wpisów z Twittera, które teraz w większości wypadków ładują się natychmiastowo
  • zamieniłem strzałki do paginacji miejscami (następna strona po prawej)
  • zmodyfikowałem listę linków w sidebarze tak aby ich kolejność była losowa
  • poprawiłem wyszukiwarkę tak aby poprawnie obsługiwała przycisk Czytaj dalej dla wpisów, które go posiadają
  • poprawiłem wyszukiwarkę tak aby pokazywała także normalną paginację, a nie tylko strzałki po bokach ekranu
  • dodałem link do profilu na LinkedIn do menu serwisów społecznościowych
  • otwarcie menu wyszukiwania ustawia teraz kursor w polu formularza
  • poprawiłem podświetlanie aktywnego elementu menu na wielu podstronach
  • formularz kontaktowych korzysta teraz z ReCAPTCHA
  • przywróciłem do życia overdocs.net
  • zmiany wizualne:
    • zwiększyłem kontrast tekstu
    • poprawiłem style portfolio (w tym responsywność)
    • poprawiłem style formularza kontaktowego
    • poprawiłem wyświetlanie menu na urządzeniach mobilnych
    • poprawiłem typografię tekstu
    • poprawiłem style nagłówków
    • poprawiłem style cytatów
    • poprawiłem style dla wielu wariantów wyświetlania list
    • poprawiłem wyświetlanie miniaturek w portfolio
    • dodałem style dla boxów z informacją/ostrzeżeniem
    • zmniejszyłem wagę fonta dla pogrubionego tekstu
    • zmniejszyłem delikatnie marginesy dla akapitów
    • dodałem zawijanie bloków kodu po 450px wysokości na urządzeniach mobilnych
    • wiele mniejszych lub zapomnianych poprawek
  • zmiany w treści (lista niepełna):
    • poprawiłem kilkadziesiąt literówek i miejsc z błędnym formatowaniem tekstu
    • poprawiłem wiele niedziałąjacych i przestarzałych linków
    • zoptymalizowałem wagę niemal wszystkich obrazków wyświetlanych na stronie
    • połączyłem podobne tagi dla wpisów
    • portfolio
      • dodałem opis projektu Pastebin
      • dodałem opis projektu Blackout
      • zaktualizowałem opis projektu Codice (opis starej wersji został wydzielony do osobnej podstrony)
      • zaktualizowałem opis projektu dla sobak.pl (opis starych wersji, opartych o WordPressa został wydzielony do osobnej podstrony)
      • zaktualizowałem opis projektu WordPress Cleaner
    • wiele pomniejszych poprawek
  • stats.sobak.pl
    • dodałem podstawowe statystyki z GitHuba
    • kliknięcie w nazwę serwisu kieruje teraz bezpośrednio do mojego profilu

Codice: obsługa migracji baz danych dla wtyczek

W poprzednim wpisie przedstawiłem zarys głównego API stojącego za obsługą rozszerzeń w Codice. Nie jest to oczywiście całość systemu, wtyczki poza integracją ze rdzeniem za pomocą akcji i filtrów muszą mieć zapewnione inne możliwości, pozwalające na swobodne operowanie. Jedną z bardziej istotnych jest wpływ na strukturę bazy danych.

W większości nowoczesnych frameworków dzieje się to za pomocą migracji, czyli opisanych w ustalony wcześniej sposób procedur kolejnych zmian w bazie danych. Dotyczy to dodawania, usuwania a także modyfikowania kolumn lub całych tabel. Przy instalacji aplikacji (także Codice) wszystkie migracje są wykonywane w kolejności ich definiowania - zapewnia to między innymi bardzo pożądaną możliwość wersjonowania struktury bazy danych.

Po tym opisie widać już że bardzo wskazane byłoby posiadanie analogicznego mechanizmu dla wtyczek. Ponieważ migracje powinno definiować się dwustronnie, można założyć że będą one aplikowane przy instalacji wtyczki, a wszelkie wprowadzone do struktury bazy zmiany będą cofane przy deinstalacji rozszerzenia (owa "druga strona" migracji).

By osiągnąć założone cele nie będziemy tworzyć zupełnie autorskiego systemu. Wszakże Laravel posiada już niemal identyczny mechanizm - brakuje tylko możliwości przypisania każdej z migracji konkretnej wtyczki. Pierwotnie próbowałem osiągnąć to za pomocą parametrów przekazywanych do komendy artisan migrate. Dokumentacja wspomina bowiem o opcji --path, pozwalającej na wykonanie migracji ograniczonych do jednej lokalizacji w systemie plików. Okazuje się jednak że w ten sposób nie możemy odwrócić migracji. Zdecydowałem się więc na rozszerzenie domyślnego repozytorium (w rozumieniu architektury aplikacji obiektowych) migracji o potrzebne wymagania.

<?php

namespace Codice\Plugins;

use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Database\Migrations\DatabaseMigrationRepository;
use Illuminate\Database\Migrations\MigrationRepositoryInterface;

class MigrationRepository extends DatabaseMigrationRepository implements MigrationRepositoryInterface
{
    protected $plugin;

    /**
     * Create a new database migration repository instance.
     *
     * @param  Illuminate\Database\ConnectionResolverInterface  $resolver
     * @param  string  $plugin  Unique application-wide plugin identifier
     */
    public function __construct(Resolver $resolver, $plugin)
    {
        $this->resolver = $resolver;
        $this->plugin = $plugin;
        $this->table = 'migrations_plugins';

        parent::__construct($resolver, $this->table);
    }

    /**
     * Get the ran migrations.
     *
     * @return array
     */
    public function getRan()
    {
        return $this->table()
            ->where('plugin', $this->plugin)
            ->orderBy('batch', 'asc')
            ->orderBy('migration', 'asc')
            ->pluck('migration')->all();
    }

    /**
     * Get list of migrations.
     *
     * @param  int  $steps
     * @return array
     */
    public function getMigrations($steps)
    {
        $query = $this->table()
            ->where('plugin', $this->plugin)
            ->where('batch', '>=', '1');

        return $query->orderBy('migration', 'desc')->take($steps)->get()->all();
    }

    /**
     * Get the last migration batch.
     *
     * @return array
     */
    public function getLast()
    {
        $query = $this->table()
            ->where('plugin', $this->plugin)
            ->where('batch', $this->getLastBatchNumber());

        return $query->orderBy('migration', 'desc')->get()->all();
    }

    /**
     * Log that a migration was run.
     *
     * @param  string  $file
     * @param  int     $batch
     * @return void
     */
    public function log($file, $batch)
    {
        $record = ['plugin' => $this->plugin, 'migration' => $file, 'batch' => $batch];

        $this->table()->insert($record);
    }

    /**
     * Remove a migration from the log.
     *
     * @param  object  $migration
     * @return void
     */
    public function delete($migration)
    {
        $this->table()->where('migration', $migration->migration)->delete();
    }

    /**
     * Create the migration repository data store.
     *
     * @return void
     */
    public function createRepository()
    {
        $schema = $this->getConnection()->getSchemaBuilder();

        $schema->create($this->table, function ($table) {
            $table->increments('id');
            $table->string('plugin');
            $table->string('migration');
            $table->integer('batch');
        });
    }
}

Nadpisywane są wyłącznie niektóre metody, dokładnie tak aby po przekazaniu do konstruktora klasy identyfikatora danej wtyczki, wykonywać operacje na powiązanych z nią migracjach. Ich wywoływanie przebiega niemal analogicznie do klasycznych migracji wbudowanych w Laravela, informacji zasięgnąłem oczywiście buszując w jego źródłach (i zdecydowanie polecam to rozwiązanie, Larwę za wiele rzeczy można słusznie skrytykować, ale w kodzie naprawdę łatwo się połapać).

// ...

// Metoda pomocnicza.
// Używamy wbudowanego migratora, przekazujemy jedynie własne repozytorium.
protected function prepareMigrator($identifier)
{
    $repository = new MigrationRepository($this->app['db'], $identifier);

    if (!$repository->repositoryExists()) {
        $repository->createRepository();
    }

    return new Migrator($repository, $this->app['db'], $this->app['files']);
}

// ...

// Przy instalacji wtyczki
$migrator = self::prepareMigrator($identifier);
$migrator->run([$plugin->path('migrations')]);

// I analogicznie przy jej odinstalowywaniu
$migrator = self::prepareMigrator($identifier);
$migrator->rollback([$plugin->path('migrations')]);

Jak widać jest to naprawdę nieskomplikowane zadanie. Nie dajcie się zmylić ilości kodu, większość z niego to delikatnie zmodyfikowane metody klasy rodzica. Co ważniejsze, kryje się za tym życiowa lekcja.

Nie wynajduj koła od nowa, zwłaszcza jeśli masz już cały zestaw narzędzi w postaci frameworka. Poświęć chwilę na zgłębienie jego działania, zaoszczędzisz wiele czasu i bólu głowy.

~Sobak, gdzieś w tym momencie

Pozdrawiam!


Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu Codice. Po więcej szczegółów zapraszam do pierwszego wpisu oraz na codice.eu. Poza planowaną od dawna serią jest to też realizacja wymogów konkursu "Daj Się Poznać". Więcej o nim możecie przeczytać w tym wpisie. Po nowe materiały poświęcone tworzeniu projektu Codice zapraszam w każdy wtorek. Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga przeczytacie co sobotę. Miejmy nadzieję, że tym razem już zgodnie z harmonogramem!

Codice: system wtyczek

Jednym z kluczowych elementów, nad którym pracowałem (i nadal pracuję) przy tworzeniu Codice jest wbudowana obsługa dla rozszerzeń. Zagadnienie, na którym nie raz zjadałem sobie zęby lub, co jeszcze gorsze, cierpiałem przez odkładanie jego implementacji na wieczne nigdy. Do dziś po dysku wala mi się jakaś wczesna wersja Codice… w bodajże 3 odmianach, różniąch się drobnymi detalami. Niby drobiazg, ale nie chcę nawet zaczynać opisywania tego, jak męczące jest równoległe utrzymanie każdego z wariantów.

Skoro próbowałem już tak wiele razy i obecne podejście do Codice miało być wreszcie tym właściwym, musiałem wreszcie zmierzyć się z tym zagadnieniem. Jednocześnie musiałem dopuścić do głosu nieco pragmatyzmu i wybrać podejście, które będzie adekwatne do mojego doświadczenia w tym temacie i na którym, mówiąc wprost, nie polegnę po raz kolejny.

W tym wpisie przedstawię najbardziej podstawowe elementy systemu wtyczek w moim projekcie, mianowicie akcje i filtry. Wielu z Was zapewne zna ten koncept z innych aplikacji (na przykład WordPressa). Wszystko opiera się na zdefiniowanych w rdzeniu hookach, do których możemy podpiąć własne zdarzenia po to, by wykonały się w określonej sytuacji. Pierwszy z brzegu przykład: rejestrujemy akcję dodającą wiadomość powitalną dla zdarzenia (hooka) user.created - utworzenia nowego użytkownika.

Innym sposobem rozszerzalności mogą być filtry - ponownie, zdefiniowane w rdzeniu miejsca, w których istotne wartości możemy później modyfikować (filtrować) za pomocą podpiętych callbacków. W przypadku Codice jest to między innymi zapytanie używane przy wyszukiwaniu notatek. Wszystko z myślą o wtyczkach, które chciałyby je zmienić np. tak aby wykorzystywało inne pola lub też wyszukiwanie pełnotekstowe z MySQL.

Nie będę więcej przedłużał, przejdę do prezentowania stojącego za tym kodu. Jeśli jakiś koncept wciąż nie jest jasny, nie martwcie się, na samym końcu zalinkuję do testów dla tej części kodu, wydaje mi się, że one powinny idealnie zobrazować zamysł i planowany sposób ich wykorzystania.

<?php

namespace Codice\Support\Traits;

use Exception;
use Log;

trait Hookable
{
    /**
     * @var array Holds all currently registered hookables
     */
    protected static $hookables;

    /**
     * Register new hookable for given hook.
     *
     * @param string $hook Name of the hook
     * @param string $hookableName Name of the hookable, must be unique within a hookable of given type
     * @param callable $callable Callable containing code to run
     * @param int $priority Order of calling hookables, when priority is equal order is undefined
     * @return bool
     */
    public static function register($hook, $hookableName, callable $callable, $priority = 10)
    {
        $hookableType = self::getHookableType();

        if (self::isRegistered($hook, $hookableName)) {
            Log::warning("$hookableType '$hookableName' was already registered within '$hook' hook and has been overwritten.");
        }

        self::$hookables[$hook][$hookableName] = [
            'priority' => $priority,
            'callable' => $callable,
        ];

        return true;
    }

    /**
     * Check whether hookable of given name has been registered within a specified hook.
     *
     * @param string $hook Name of the hook
     * @param string $hookableName Name of the hookable within a hook
     * @return bool
     */
    public static function isRegistered($hook, $hookableName)
    {
        return isset(self::$hookables[$hook][$hookableName]);
    }

    /**
     * Deregister hookable from given hook.
     *
     * Deregistration must happen before call() method is run to have an effect.
     *
     * @param string $hook Name of the hook
     * @param string $hookableName Name of the hookable
     * @return bool
     */
    public static function deregister($hook, $hookableName)
    {
        unset(self::$hookables[$hook][$hookableName]);

        return true;
    }

    /**
     * Get (sorted) list of all hookables assigned to a hook.
     *
     * @param string $hook Name of the hook
     * @return array
     */
    private static function getHookables($hook)
    {
        $hookables = isset(self::$hookables[$hook]) ? self::$hookables[$hook] : [];

        return self::sortHookables($hookables);
    }

    /**
     * Sort hookables by their priority.
     *
     * @param array $hookables Array of unsorted hookables
     * @return array
     */
    protected static function sortHookables($hookables)
    {
        usort($hookables, function ($a, $b) {
            if ($a['priority'] == $b['priority']) {
                return 0;
            }

            return $a['priority'] < $b['priority'] ? -1 : 1;
        });

        return $hookables;
    }

    /**
     * Return human readable name of hookable
     *
     * @throws Exception
     * @return string
     */
    protected static function getHookableType()
    {
        throw new Exception('getHookableType() must be implemented');
    }
}

Zaczynamy od… traita? Dokładnie tak. Ponieważ główną różnicą między akcjami a filtrami jest to, że pierwsze nie mogą zwracać wartości, a drugie muszą to robić, możemy śmiało stwierdzić że spora część kodu będzie się powtarzać. Dlatego też wszystkie wspólne funkcjonalności, takie jak rejestrowanie konkretnych zdarzeń, wywoływanie ich czy potrzebne w obu wypadkach metody pomocnicze zostały umieszone w jednym miejscu, jak pokazano wyżej.

Jak sami zobaczycie, dzięki temu kod odpowiedzialny za działanie akcji i filtrów będzie naprawdę krótki i prosty. Jest to kolejno:

<?php

namespace Codice\Plugins;

use Codice\Support\Traits\Hookable;

class Action
{
    use Hookable;

    /**
     * Call all actions registered for a given hook.
     *
     * @param string $hook Name of the hook
     * @param array $parameters Parameters which will be passed to callback
     */
    public static function call($hook, array $parameters = [])
    {
        $actions = self::getHookables($hook);

        foreach ($actions as $action) {
            call_user_func($action['callable'], $parameters);
        }
    }

    /**
     * @inheritdoc
     */
    protected static function getHookableType()
    {
        return 'Action';
    }
}

oraz

<?php

namespace Codice\Plugins;

use Codice\Support\Traits\Hookable;

class Filter
{
    use Hookable;

    /**
     * Call all filters registered for a given hook.
     *
     * @param string $hook Name of the hook
     * @param mixed $value Value to run filters on
     * @param array $parameters Parameters which will be passed to callback
     * @return mixed Filtered value
     */
    public static function call($hook, $value, array $parameters = [])
    {
        $filters = self::getHookables($hook);

        foreach ($filters as $filter) {
            $value = call_user_func($filter['callable'], $value, $parameters);
        }

        return $value;
    }

    /**
     * @inheritdoc
     */
    protected static function getHookableType()
    {
        return 'Filter';
    }
}

Jak na dłoni widać teraz różnice w działaniu obu hookowalnych (zaczepialnych?) elementów. Tak więc raz jeszcze - w obu wypadkach wykonujemy wszystkie zarejestrowane (przez pluginy bądź samą aplikację) zdarzenia, czyniąc to według określonego priorytetu. Akcje po prostu wykonują kod, a filtry kolejno modyfikują przekazaną wartość.

Jak już mówiłem, najszybszym sposobem na zobaczenie ich w akcji jest spojrzenie na testy - osobno dla akcji oraz filtrów. Przykłady użycia podstawowego API do wtyczek przez samą aplikację znajdują się tutaj, nazwy plików powinny być raczej jasne ;)


Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu Codice. Po więcej szczegółów zapraszam do pierwszego wpisu oraz na codice.eu. Poza planowaną od dawna serią jest to też realizacja wymogów konkursu "Daj Się Poznać". Więcej o nim możecie przeczytać w tym wpisie. Po nowe materiały poświęcone tworzeniu projektu Codice zapraszam w każdy wtorek. Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga przeczytacie co sobotę. Miejmy nadzieję, że tym razem już zgodnie z harmonogramem!

Codice: weryfikacja właściciela zasobu (uprawnień użytkownika)

Z kilku przyczyn delikatnie rozjechał mi się harmonogram cotygodniowy, ale czas nadrobić straty. Poprzednio opisywałem doświadczenia zebrane podczas projektowania procesu instalacji dla Codice. Zajmiemy się teraz kolejnym zagadnieniem, które mimo iż oparte na przykładzie tego konkretnego projektu, jest jednym z podstawowych zagadnień dla każdej aplikacji CRUD. Zachęcam do tego aby ten, jak i kolejne wpisy traktować jako zwyczajne poradniki dla piszących w Laravelu, z nutką historii w tle.

Za każdym razem gdy piszemy aplikację, której instancja ma być używana równolegle przez więcej niż jedną osobę, pojawia się kwestia weryfikacji właściciela zasobu. Prościej mówiąc - sprawdzenia czy obecnie zalogowany użytkownik ma uprawnienia do wyświetlenia/edycji/usunięcia danego rekordu. Na pierwszy rzut oka sprawa nie należy do skomplikowanych:

$note = Note::where('id', $request->get('id'))->where('user_id', Auth::id())->get();

Na przykładzie notatek, pobieramy rekord o ID przekazanym w adresie i zaznaczamy że ID przypisanego użytkownika musi być równe identyfikatorowi obecnie zalogowanego (metoda pomocnicza id() z fasady Auth). Przyjmijmy też że naszym celem jest usunięcie wybranej notatki. W razie błędu przekierujemy zaś na inną stronę, na której wyświetlimy czytelny dla użytkownika komunikat. Bardziej kompletny kod oparty o metodzie przedstawionej powyżej mógłby wyglądać tak:

$note = Note::where('id', $request->get('id'))->where('user_id', Auth::id())->get();

if ($note === null) {
    return redirect('/')->with('error', 'Nie jesteś właścicielem podanej notatki');
}

$note->delete();

return redirect('/')->with('success', 'Notatka usunięta');

W wypadku gdy nie uda się odnaleźć pasującego rekordu otrzymujemy null. Wiemy wtedy że podana notatka nie istnieje lub istnieje, ale należy do kogoś innego (wymagamy spełnienia obu warunków), możemy więc cofnąć użytownika i wyświetlić mu odpowiednią wiadomość.

Na dobrą sprawę wpis można by zakończyć już w tym miejscu. Warto jednak spojrzeć bardziej perspektywicznie. Podany fragment kodu będzie się przecież powtarzał także dla innych operacji CRUD (np. edycji lub odczytu), nasza aplikacja może też zarządzać więcej niż jednym rodzajem danych (na przykładzie Codice - notatkami oraz etykietami). W efekcie otrzymamy brzydką i niewygodną w utrzymaniu powtarzalność kodu. Zastanówmy się jak można to poprawić.

Rozsądne wydaje się skorzystanie z metody find(), która przyjmuje za konwencję, że polem, po którym będziemy indeksować tabelę jest id. Możemy połączyć to także z możliwością opisaną w dokumentacji Laravela jako query scopes. Do naszego modelu dodajemy:

public function scopeMine($query)
{
    return $query->where('user_id', '=', Auth::id());
}

a wynikowo naszą metodę kontrolera redukujemy do postaci

$note = Note::mine()->find($request->get('id'));

if ($note === null) {
    return redirect('/')->with('error', 'Nie jesteś właścicielem podanej notatki');
}

$note->delete();

return redirect('/')->with('success', 'Notatka usunięta');

Jest lepiej. Powtarzalna część zapytania została przeniesiona do modelu (oraz konwencji używanej przez ORM Laravela). Wciąż jednak widzimy niezmienną część logiki, którą także przydałoby się wypchnąć na zewnątrz, w jakieś wspólne miejsce.

W tym celu możemy wykorzystać wyjątki, bazując zresztą na logice używanej przez wbudowaną metodę findOrFail(). Do naszego modelu dodajemy kolejną metodę pomocniczą:

public static function findMine($id)
{
    $note = self::mine()->find($id);

    if (!$note) {
        throw new NoteNotFoundException;
    }

    return $note;
}

W obrębie aplikacji tworzymy wyjątek NoteNotFoundException i importujemy go w modelu.

<?php

namespace App\Exceptions;

class NoteNotFoundException extends \Exception {}

Logikę obsługi błędów przenosimy natomiast do handlera wyjątków, edytując metodę render() w klasie AppExceptionsHandler.

public function render($request, Exception $e)
{
    if ($e instanceof NoteNotFoundException) {
        return Redirect::route('index')->with('message', 'Nie jesteś właścicielem podanej notatki`);
    }

    // reszta domyślnego kodu, np. ModelNotFoundException
}

Teraz kod użyty w kontrolerze powinien być naprawdę krótki i satysfakcjonujący:

$note = Note::findMine($request->get('id'));

$note->delete();

return redirect('/')->with('success', 'Notatka usunięta');

Jest dobrze? Tak. Może być lepiej? Oczywiście! Wady rozwiązania ujawnią się przy jakiejkolwiek większej skali. Pozostając przy Codice - ten sam kod musiałby być zduplikowany dla notatek i etykiet - dwa różne wyjątki, dwa bloki obsługujące go w handlerze, analogiczne modyfikacje w obu modelach.

Skupimy się więc na wyłączeniu części wspólnych. Wyeliminujemy potrzebę tworzenia indywidualnych wyjątków dla każdego rodzaju danych, a metody wymagane dla każdego z modeli przeniesiemy do traita.

<?php

namespace Codice\Support\Traits;

use Auth;
use Illuminate\Http\Exceptions\HttpResponseException;

trait Owned
{
    /**
     * Find owned model (limited to the current user).
     *
     * @param  int $id Model ID
     * @return static
     */
    public static function findMine($id)
    {
        $model = self::mine()->find($id);

        if (!$model) {
            $modelLangFile = array_reverse(explode('\\', get_called_class()))[0];

            throw new HttpResponseException(redirect()->route('index')->with([
                'message' => trans("$modelLangFile.not-found"),
                'message_type' => 'danger',
            ]));
        }

        return $model;
    }

    /**
     * Query scope for getting owned models (limited to the current user).
     *
     * @param $query Illuminate\Database\QueryBuilder
     * @return Illuminate\Database\QueryBuilder
     */
    public function scopeMine($query)
    {
        return $query->where('user_id', '=', Auth::id());
    }

    /**
     * Define relationship to the User owning the model.
     */
    public function user()
    {
        return $this->belongsTo('CodiceUser');
    }
}

W tym wypadku obsługa po stronie kontrolera wygląda dokładnie tak, jak pokazano wyżej, jednak cała implementacja logiki dla określonego modelu ogranicza się do use Owned, bez żadnego powtarzania schematycznego kodu. Dodatkowym klasom wyjątków również możemy już podziękować, korzystamy z wbudowanego HttpResponseException, więc AppExceptionsHandler może zostać przywrócone do wcześniejszej postaci. Dodatkowo mamy też własny scope mine() (przydatny przy innych rodzajach zapytań niż wybranie pojedynczego rekordu po ID) oraz zdefiniowaną relację do modelu użytkownika.

Ciekawostką jest hack w metodzie findMine(), który pobiera nazwę modelu wywołującego metodę i na tej podstawie buduje klucz do pliku z tłumaczeniami (w celu zróżnicowania komunikatów błędów, np. "nie znaleziono notatki" vs "nie znaleziono etykiety").

Przedstawiłem kilka rożnych metod, skracając nieco drogę, przez którą ten kawałek kodu przeszedł w moim własnym projekcie. Zainteresowanych sprawdzeniem faktycznego kodu mogę odesłać do historii zmian w kontrolerze notatek oraz oczywiście traita Owned


Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu Codice. Po więcej szczegółów zapraszam do pierwszego wpisu oraz na codice.eu. Poza planowaną od dawna serią jest to też realizacja wymogów konkursu "Daj Się Poznać". Więcej o nim możecie przeczytać w tym wpisie. Po nowe materiały poświęcone tworzeniu projektu Codice zapraszam w każdy wtorek. Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga przeczytacie co sobotę.

Laravel i podpowiedzi IDE? To możliwe

Nie ukrywajmy - magii w Laravelu jest dużo. Na tyle dużo, że można ją kochać bądź nienawidzić - nie wyobrażam sobie stanu pośredniego. Abstrahując zupełnie od tego, czy takie rozwiązanie jest słuszne, trzeba mu wytknąć jedną, definitywną wadę. Zintegrowane środowiska programistyczne (IDE) w kontakcie z nią po prostu wariują.

Nie ma problemu dopóki korzystamy z prostych edytorów tekstowych typu Sublime Text, Atom czy Visual Studio Code. Więszkość z nich albo w ogóle nie dostarcza podpowiedzi dla kodu PHP lub są one mało "inteligentne". Bazują na prostym słowniku użytych symboli (nazw funkcji, klas czy zmiennych) i wyszukiwarce. Tutaj rysuje się jedna z głównych różnić pomiędzy edytorem, a IDE - środowisko programistyczne stara się "zrozumieć kod". Za pomocą różnych zaawansowanych parserów stara się przewidzieć zachowanie faktycznego interpretera PHP i jako podpowiedzi podaje nam tylko to, czego możemy faktycznie użyć w danym kontekście, bez spowodowania błędu. Nie dostaniemy w podpowiedzi zmiennej, która jest niedostępna w danym zasięgu, ponieważ PHP jako interpreter też nie byłby jej w stanie użyć.

Mimo całej swojej "inteligencji" IDE nie odwzorowują jednak w stu procentach zachowania języka - złożoność takiego programu byłaby naprawdę ogromna, a prędkość działania niezadowalająca. Dlatego występują sytuacje, w których dany kod w rzeczywistości działa, a IDE po prostu się "gubi". Nie przetwarza wszystkich dynamicznych sytuacji obecnych na przykład przy użyciu wspomnianej magii języka.

Z opisaną sytuacją spotkał się na pewno każdy, kto próbował używać projektu napisanego w Laravelu w którymkolwiek z popularnych środowisk programistycznych dla PHP, na przykład PhpStormie. Zaznaczę że mówimy tu głównie o wykorzystaniu fasad (w rozumieniu Laravela), których można unikać - promuje je jednak sama dokumentacja. W praktyce program pokazuje tak wiele błędów, że żeby nie rozpraszać się nimi trzeba by wyłączyć praktycznie wszystkie podpowiedzi (lub tzw. inspekcje). Przyznacie jednak, że pozbawienie IDE całej jego "inteligencji" i tym samym zrobienie z niego zwyczajnego edytora trochę mija się z celem.

Czy jest na to jakieś rozwiązanie? Oczywiście.

W przypadku Laravela możemy użyć bardzo popularnej paczki laravel-ide-helper. Analizuje ona kod frameworka i generuje plik będący swego rodzaju "mapą" pomiędzy fasadami, prostymi interfejsami używanymi przez programistę, a normalnymi klasami, których wewnętrznie używa framework, komunikując te dwie warstwy za pomocą sporej ilości skomplikowanych odwołań i opisywanej magii.

Tak wygenerowany plik, obecny w katalogu projektu wystarcza aby więszkość środowisk potrafiła nadążyć za sztuczkami zastosowanymi w Laravelu. Nie tylko przestała pokazywać błędy, ale faktycznie zaczęła pomagać, na przykład podpowiadając dostępne metody i ich parametry.

Zaczynamy od zainstalowania paczki przy użyciu Composera. Myślę że dowolnej osobie używającej Laravela czy innego nowoczesnego frameworka PHP nie trzeba tłumaczyć obsługi tego narzędzia.

composer require --dev barryvdh/laravel-ide-helper

Zwróćcie uwagę że instalujemy paczkę jako zależność deweloperską. Skutkuje to tym, że w środowisku produkcyjnym w ogóle nie będzie ona pobierana. Teraz przechodzimy do kolejnego kroku, czyli zarejestrowania Service Providera, który komunikuje paczkę z frameworkiem. Tutaj też odbiegniemy nieco od standardowej metody, czyli dodania wpisu w config/app.php i rejestracji Service Providera dokonamy warunkowo. Dlaczego? Ponieważ w przeciwnym razie, w środowisku produkcyjnym (a więc pominięciu instalacji tej paczki) framework próbowałby użyć nieistniejącej klasy. Dlatego też otworzymy plik app/providers/AppServiceProvider.php i w metodzie register() dodamy następujący kod:

public function register()
{
    if ($this->app->environment() !== 'production') {
        $this->app->register(Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
    }
    // ...
}

Dzięki temu framework nie będzie próbował rejestrować Service Providera na produkcji.

Następnie możemy już skorzystać z udostępnionych nam komend.

php artisan clear-compiled
php artisan ide-helper:generate
php artisan optimize

Paczka udostępnia nieco więcej opcji, włącznie z generowaniem dokumentacji dla naszych modeli, jednak nie są one generowane domyślnie. Po szczegóły raz jeszcze odsyłam do oficjalnego repozytorium

W głównym katalogu projektu powstanie plik _ide_helper.php. Nie musimy go nigdzie dołączać, w tym momencie wszystko powinno już działać. Ów plik zdecydowanie dodajmy do ignorowanych przez Gita, o ile z niego korzystamy (a przecież wszyscy to robimy, prawda?). W najprostszym wariancie dodajemy linię /_ide_helper.php do .gitignore. O tym, dlaczego niekoniecznie jest to najlepszy pomysł, postaram się napisać już niedługo ;)


Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu Codice. Po więcej szczegółów zapraszam do pierwszego wpisu oraz na codice.eu. Poza planowaną od dawna serią jest to też realizacja wymogów konkursu "Daj Się Poznać". Więcej o nim możecie przeczytać w tym wpisie. Po nowe materiały poświęcone tworzeniu projektu Codice zapraszam w każdy wtorek. Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga przeczytacie co sobotę.

Codice: instalator webowy dla aplikacji opartej o Laravel

Dośc długo zastanawiałem się czemu mógłbym poświęcić swój pierwszy, faktyczny wpis poświęcony rozwojowi Codice i problemom jakie musiałem rozwiązać. Daruję sobie rozpoczynanie od struktury katalogów, tworzenia kontrolerów i podobnych zadań — zaczniemy od początku, ale z perspektywy użytkownika.

Dość dużo mówi się o tym, jak zmienił się rozwój aplikacji webowych w ostatnich latach. Kiedyś (czy to patrząc na rok, czy na doświadczenie konkretnego programisty) prawdpodobnie wszyscy zaczynaliśmy, w wypadku PHP, od kilku plików na krzyż, w ekstremalnych przypadkach wrzucając jakąś rozpakowaną bibliotekę do osobnego folderu. Dzisiaj rozwój projektów, zwłaszcza na początku, przypomina budowę z klocków Lego. Zaczynamy od zainstalowania całego środowiska programistycznego — serwera (lub programu do obsługi środowisk zwirtualizowanych), Composera do obsługi zależności PHP, menedżera zależności dla JavaScript popularnego akurat w danym tygodniu (pun intended ;) ) i dopiero ostatecznie przystępujemy do pisania faktycznego kodu.

Ze stawianiem już gotowej aplikacji jest całkiem podobnie. Pobieramy kod, a następnie zdefiniowane w projekcie zależności. Potem jest już z górki — kompilujemy assety dla frontendu (w końcu to tylko kwestia odpalenia task runnera), wypełniamy plik konfiguracyjny (lub ustawiamy zmienne środowiskowe), uruchamiamy migracje dla bazy danych, jeśli nam zależy to wrzucamy do cache co się da — na koniec tworzymy konto użytkownika i voilà. Proste, prawda?

A no właśnie nie bardzo. Wszystkie z opisanych rozwiązań to ogromny krok naprzód i ułatwienie dla programistów. Co więcej, przekładające się bezpośrednio na jakość ostatecznego produktu. Nie przerzucajmy jednak całego procesu ustawiania środowiska na użytkownika. Mamy dostępnych tyle świetnych technik i narzędzi, a tymczasem proces instalacji przeciętnego skryptu PHP się uwstecznił. W wypadku Codice postanowiłem połączyć zdobycze naszych czasów z prostotą instalacji dawnych skryptów.

Czynności do wykonania możemy podzielić na trzy grupy:

  • wykorzystanie zewnętrznych narzędzi — node.js wraz z Yarnem zarządzającym zależnościami dla tej technologii i gulpem odpowiedzialnym za przygotowanie zasobów dla client-side, ale także Composer dla zależności PHP
  • wypełnienie plików konfiguracyjnych
  • uruchomienie procesów oskryptowanych w PHP — na przykład migracji dla struktury bazy danych

Pierwszy krok zrealizowałem dzięki udostępnieniu do pobrania wstępnie zbudowanej paczki plików. W archiwum znajdują się produkcyjne zależności PHP, a także skompilowane assety (nie ma za to node_modules i innych plików wykorzystywanych wyłącznie przy developmencie — szanujmy łącza i czas użytkowników).

Resztą zajmuje się już instalator, będący de facto zwyczajnym kontrolerem Laravela. Aplikację zainstalowaną od tej jeszcze niezainstalowanej postanowiłem rozróżniać sprawdzajac obecność pliku .env. Ponieważ jednak jest on tworzony na pewnym etapie instalacji, potrzebny był dodatkowy sposób oznaczenia obecnie trwającej instalacji (tak, aby po kroku wypełniającym .env nie wyrzuciło nas z instalatora) — jest to po prostu plik tymczasowy tworzony po wejściu do instalatora i usuwany na jej ostatnim kroku. Cały proces wygląda następująco:

public function __construct()
{
    if (file_exists(base_path('.env')) && !file_exists(storage_path('app/.install-pending'))) {
        $this->denyInstallation = true;
        return Redirect::route('index')->send();
    }

    // ...
}

Kod znajduje się w konstruktorze więc automatycznie jest wykonywany dla każdego z kroków (akcji) instalatora. Warto zwrócić uwagę na użycie metody send(), jest ona wymagana w wypadku próby obsługi żądań HTTP z poziomu konstruktora.

Kolejnym etapem jest sprawdzenie wymagań, tj. obecności rozszerzeń PHP i zapisywalności określonych katalogów. Nie ma tu żadnej filozofii także przejdę dalej, do wypełniania pliku .env. Z pozoru proste zadanie, ot umieszczenie określonego stringa w pliku tekstowym. Jest jednak pewna pułapka, która swego czasu spędziła mi sen z powiek na ładnych parę godzin…

Problem pojawia się gdy w instalatorze wykorzystujemy jakiekolwiek sesje bądź ciasteczka (w moim wypadku było to ustawienie języka instalacji). Zarówno sesje jak i ciasteczka obsługiwane przez Laravela są szyfrowane przy użyciu klucza, tzw. APP_KEY. Jest on indywidualny dla każdej aplikacji, a więc przechowywany właśnie w pliku .env. Ten jednak nie istnieje na samym początku instalacji. Co wtedy robi Laravel? Spójrzmy na plik config/app.php:

'key' => env('APP_KEY', 'SomeRandomStringSomeRandomString'),

W wypadku gdy zmienna nie istnieje, klucz jest ustawiany na wartość domyślną, podaną w drugim parametrze funkcji. To jest właśnie klucz szyfrujący ciastka i sesje na poczatku instalacji… aż do momentu gdy, wypełniając .env, wstawimy do niego nowy klucz, który własnie wygenerowaliśmy. Skutek można sobie łatwo wyobrazić - dane w nich zapisane stają się niemożliwe do odczytania. Mając tę wiedzę spokojnie możecie wymyślić kilka różnych obejść problemu — ja zdecydowałem się doczepić aktualnie wybrany język do adresu, na który przekierowuję po wypełnieniu pliku konfiguracyjnego. W kolejnym kroku mogę go z tego adresu odczytać i ponownie zapisać do sesji.

W ten sposób doszliśmy do kolejnej kwestii, która na pierwszy rzut oka może wydawać się problematyczna — uruchomienie migracji z poziomu skryptu PHP. Domyślnie jest to bowiem komenda artisana, a dokumentacja zdaje się nie wspominać o alternatywnych drogach. Również wymóg użycia exec() dla zwykłej instalacji wydaje się przesadny, wszkaże wiele hostingów nadal blokuje tę i podobne funkcje. Tym razem jednak rozwiązanie jest wyjątkowo proste, a z pomocą przychodzi nam Artisan::call(), który wykonuje kod danej komendy bez jakiegokolwiek używania warstwy linii komend.

Artisan::call('migrate', ['--force' => true]);

Pamiętajmy o użyciu opcji --force, jest ona wymagana aby migracje mogły być uruchomione w środowisku produkcyjnym.

W wypadku Codice to już praktycznie koniec, pozostało tylko utworzenie konta dla użytkownika i dodanie mu notatki powitalnej. W swoich aplikacjach możecie oczywiście wykonać wszystkie inne czynności związane z instalacją, tak aby jak najbardziej ułatwić życie Waszych użytkowników końcowych. Naprawdę nie ma potrzeby wymagać od nich znajomości typowo programistycznych narzędzi ;) Ponadto, stosując przedstawione tutaj rozwiązanie nie blokujemy żadnej z dotychczasowych dróg instalacji. Nic nie stoi na przeszkodzie aby pobrać źródła (lub sklonować repozytorium) i wszystkie czynności wykonać ręcznie.

Na koniec, wszystkim zainteresowanym dokładnym działaniem instalatora polecam zacząć przeglądanie kodu od tego kontrolera. Aby zamieszać jeszcze troszkę, dodam że Codice obsługuje też instalację z poziomu CLI, dla osób które chciałyby postawić całą instalację bez opuszczania terminala bądź szukają wygodnej drogi na zautomatyzowanie procesu. Odpowiedzialny za to kod znajdziecie w tym miejscu.


Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu Codice. Po więcej szczegółów zapraszam do pierwszego wpisu oraz na codice.eu. Poza planowaną od dawna serią jest to też realizacja wymogów konkursu "Daj Się Poznać". Więcej o nim możecie przeczytać w tym wpisie. Po nowe materiały poświęcone tworzeniu projektu Codice zapraszam w każdy wtorek. Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga przeczytacie co sobotę.